Compare commits

...
28 Commits
Author SHA1 Message Date
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
coolguyandClaude Opus 5 5ac899df68 docs: README
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 15:59:53 +09:00
coolguyandClaude Opus 5 91f3840d19 lint: 파서 오류 복구와 두 lint — 남은 부채를 턴다
파서 오류 복구:
항목 단위로만 회복한다. 문 단위로 더 잘게 회복하려 하면 파서가 추측을
하게 되고, 틀린 추측은 없는 오류를 지어낸다. 한 항목에 오류 하나가
상한이라는 것은 정직한 한계다. 동기화 지점은 중괄호 깊이 0 + 줄 첫머리
+ 선언 시작 토큰 — 셋 다 필요하다. 본문 안의 fn을 새 항목으로 오인하면
그 뒤가 전부 어긋난다. 샘플 08이 이제 오류 넷을 한 번에 보고한다.

두 lint (취향이 아니라 비용이다):
- 미사용 import는 재검사 범위를 넓힌다. 쓰지 않는 모듈의 시그니처가
  바뀌면 이 모듈이 재검사된다.
- effect 과잉 선언은 호출자에게 없는 의무를 지운다. 시그니처는 실제보다
  좁아도 안 되고 넓어도 안 된다.

과잉 선언은 effect 변수가 있거나 본문에 모르는 이름이 있으면 판정하지
않는다. 첫 구현이 샘플 03/06을 오탐으로 잡았는데, 원인이 외부 타입이었다
— 외부 capability의 메서드는 effect를 모르므로 "수행하지 않았다"고 말할
근거가 없다. saw_unknown으로 판정을 보류한다.

Resolve.error에 blocking을 나눴다. 이름 해소 실패는 뒤 단계를 막지만
lint는 막지 않는다 — lint 하나가 진짜 타입 오류를 가리면 루프가 느려진다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 15:53:35 +09:00
coolguyandClaude Opus 5 5593b54772 std: 표준 라이브러리 — effect 다형성이 처음으로 검사된다
std/list.cool, string.cool, int.cool, bool.cool. 본문 없는 선언이고
런타임이 구현한다. 이 파일들은 구현이 아니라 시험대다.

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 15:47:44 +09:00
coolguyandClaude Opus 5 1c4f46e5e5 interp: 얇은 typed IR과 트리 워킹 인터프리터 — coolc run
문서에만 있던 실행 의미가 코드가 된다.

- ir.ml: AST를 얇은 IR로 낮춘다. `?`는 Result에 대한 match로 펼쳐지고,
  타입 인자는 사라지며(단형화 없음), 한정 이름은 하나의 이름으로 접힌다.
  이름은 낮추기 시점에 분류된다 — 실행 중에 "지역인가 전역인가"를 다시
  묻지 않는다.
- interp.ml: 검사하지 않는 인터프리터. 여기 도달한 프로그램은 이미 타입,
  effect, capability, ownership 검사를 통과했고, 같은 질문을 두 번 묻는
  것은 두 번째 진실을 만드는 일이다.

권한의 유일한 출처는 런타임이다. 소스에는 capability를 만드는 문법이 없고,
main은 자기가 선언한 것만 받는다. 파라미터에서 Console을 지우면 출력할
방법이 프로그램 안에 없다 — 보안 정리 (i)의 실행 시점 대응물. TaskScope의
뿌리도 같은 이유로 런타임이 준다.

scope의 v0 실행 의미는 순차다. 구조가 먼저고 병렬성은 그 위의 최적화다.

samples/run/은 이제 "검사를 통과한다"가 아니라 "이 값을 낸다"까지 말하고,
test/가 같은 것을 검사한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 14:58:05 +09:00
coolguyandClaude Opus 5 120ff361cf naming: 툴체인 바이너리를 coolc로
`cool`은 이 머신에서 이미 다른 물건이다 (프린터/리포트 멀티툴). 그 도구의
철학이 "새 도구는 서브커맨드지 별도 설치가 아니다"인데, 언어 컴파일러를
거기 얹는 것은 그 문장의 뜻이 아니다.

컴파일러 관례를 따른다 — rustc, ocamlc, tsc. 언어 이름은 coollang 그대로.
나중의 coolfmt, coolls도 같은 자리에 붙는다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 14:53:16 +09:00
coolguyandClaude Opus 5 c4662ab627 bench: 증분 루프를 측정으로 증명한다
100k줄 200모듈에서 본문 수정은 downstream을 한 칸도 건드리지 않고(재검사
1개, 0.8ms), 시그니처 수정은 hash가 변한 곳까지만 전파되어(재검사 2개,
2.1ms) downstream 소진성 위반을 실제로 잡는다.

"빠르다"가 아니라 "다시 볼 것이 적다"가 주장이므로, 시간보다 재검사된
모듈 수를 먼저 출력한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 14:12:35 +09:00
coolguyandClaude Opus 5 801a7b330b modules: 모듈 경계, interface hash, 그리고 고정점 invalidation
되돌리기 비싼 결정 중 마지막 하나 — incremental 아키텍처 — 를 코드와
테스트로 닫는다.

- iface.ml: exported surface 추출과 해시. 별칭 한정(qualify)은 소비 시점에만
  일어나므로 가져오는 쪽의 별칭이 정의 모듈의 hash에 새지 않는다.
- session.ml: 모듈 로딩과 고정점 전파. hash 비교가 dependents 재검사보다
  앞선다 — 이 순서가 "본문만 수정 시 downstream 0건"의 전부다.
- 한정 이름(Alias.Type, Alias.Ctor, Alias.fn)을 타입 검사, 패턴, 소진성,
  move 검사가 모두 하나의 키("Alias.name")로 본다.
- 패키지 경로(cool.dev/std/list)는 v0에서 해소하지 않고 불투명하게 둔다.
  없다고 말하지 않는다.
- 회귀 테스트: 본문만 고치면 자기 자신만 재검사(1건), variant를 추가하면
  downstream까지 전파되고 실제로 소진성이 깨진다(2건).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 14:11:30 +09:00
coolguyandClaude Opus 5 5218bc59a7 exhaust: match exhaustiveness와 도달 불가 팔 검사
철학 1이 나열한 다섯 항목 중 비어 있던 자리를 채운다. Maranget의 usefulness
알고리즘으로 반례를 만들어 "빠진 경우"를 이름으로 말한다 — 중첩된 자리의
반례도 찾는다(Some(Rect(_, _))).

이 검사가 왜 지금 필요한가: interface hash가 enum 정의 본문을 입력으로 삼는
이유가 바로 이것이다. upstream에 variant가 하나 늘면 downstream의 match가
깨져야 하는데, 검사가 없으면 깨질 것이 없다. 다음 마일스톤(모듈 경계를 넘는
재검사)의 핵심 시나리오가 여기에 걸려 있다. 테스트로 그 시나리오를 직접
고정했다 — 같은 코드가 variant 둘일 때는 통과하고 셋이 되면 깨진다.

구현 중 한 번 틀렸다. 리터럴 패턴을 와일드카드로 줄였더니 Int 리터럴 두 개로
match가 완전해져 버렸다. 리터럴은 인자 없는 생성자이고, 타입의 생성자 집합이
무한하므로 리터럴만으로는 결코 완전해지지 않는다.

생성자 집합을 알 수 없는 타입(외부 타입, 미지수)은 검사하지 않는다.
모르는 것을 위반이라고 말하지 않는다.

definite init은 문법이 이미 보장한다는 것을 문서에 적었다 — let이 항상
초기화식을 요구하므로 별도 검사가 필요 없다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 03:23:47 +09:00
coolguyandClaude Opus 5 2e67b74376 move: move/affinity 검사 — v0 fast path 완성
보안 정리 (ii) — safe code에서 capability는 복제·위조되지 않는다 — 를 코드로
닫는다. 검사는 전부 함수 로컬 데이터플로우이고 전역 분석이 없다.

affinity의 뿌리는 capability다. 필드로 가진 타입은 전이적으로 affine이며
고정점까지 돌려 상호 재귀 타입도 유도한다. 이 전이가 없으면 wrapper 하나를
복사해 capability가 사실상 복제되므로 정리가 깨진다. copyable 선언과 affine
필드의 공존은 오류다.

구현한 규칙:
- affine 값은 소유 자리로 갈 때 move된다(own 파라미터, 반환, struct 저장,
  컨테이너 삽입, let 바인딩, by-move capture). moved 이후 사용은 오류이고
  진단이 어디서 소비됐는지를 말한다
- 분기 병합은 보수적 합집합. 한 분기에서라도 moved면 병합 이후 moved
- 빌린 값은 탈출하지 못한다: 반환, struct 저장, 소유 자리로 넘기기 전부 거부
- use의 전염: 빌린 값을 capture한 클로저는 그 자체가 빌린 값이라 소유 자리로
  갈 수 없다. 별도의 nonescaping 개념 없이 use 규칙 하나로 닫힌다
- callable affinity: affine 값을 capture한 클로저는 affine fn이며 fn 자리에
  갈 수 없다

자율 결정 둘:
- 클로저는 mut 바인딩을 capture할 수 없다. spawn만 막는 특수 규칙 대신
  일반 규칙으로 뒀다 — v0에 참조가 없으므로 별칭도 조용한 복사도 만들 수
  없고, spawn 제한은 이 규칙의 특수 사례가 된다
- v0에 부분 move는 없다. 필드 접근은 빌림이고 결과도 빌린 값이다.
  affine 필드만 꺼내려면 부분 move 상태 추적이 필요한데 v0가 살 복잡도가 아니다

05를 자족적으로 다시 썼다. affinity의 뿌리가 capability라 자원 타입을 모듈
안에서 정의해야 검사기가 affine임을 유도할 수 있다. 외부 타입은 affine임을
증명할 수 없으므로 copyable로 본다.

이로써 fast path(L0 parse / L1 type·effect·capability·ownership)가 완성됐다.
cool check가 처음으로 성공을 선언한다 — 01~04, 06, 07이 exit 0으로 통과한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 03:01:05 +09:00
coolguyandClaude Opus 5 79e4ed4190 effects: effect/capability 검사
문서가 "사활"이라고 지목한 단계다. effects 절이 여기서부터 장식이 아니라
검사 대상이 된다.

핵심 규칙 — 미선언 effect = compile error(철학 1). 수행한 자리를 들고
다니므로 진단이 함수 머리가 아니라 실제로 수행한 줄에 붙는다.

effect가 흐르는 경로 넷을 모두 막았다:
- capability 메서드 호출이 그 메서드의 선언된 effect를 요구한다
- 함수 호출이 그 함수의 effect를 물려준다
- 클로저의 effect는 정의한 자리가 아니라 부르는 자리에서 일어난다.
  클로저를 만들기만 하는 것은 effect가 아니고, 인자로 넘겨 호출되는 순간
  호출자의 것이 된다
- 파라미터가 허용한 범위를 넘는 함수를 넘기면 거부한다

effect 변수는 결정 위치에서 인자의 effect로 묶인다. 순서가 중요해서 한 번
틀렸다 — 포함 검사를 unify보다 먼저 하면 아직 해소되지 않은 미지수를 제약으로
오해해 정당한 코드를 거부한다. unify가 먼저고, 남는 차이만이 위반이다.
결정되지 않은 미지수는 판정을 미룬다 — 모르는 것을 위반이라고 말하지 않는다.

보안 정리 (i)을 직접 구현했다: capability 메서드는 값을 통해서만 부를 수
있다. 타입 이름으로 부를 수 있으면 capability 없이 effect를 수행하게 되어
정리가 무너진다.

effect 검사는 타입 검사와 같은 순회에서 돈다. effect 변수의 해소가 타입
변수와 같은 지점에서 일어나므로 떼어내면 순회와 인스턴스화를 두 번 한다.
소유는 나뉘되 순회는 하나다 — 문서에 근거를 적었다.

10_effect_errors.cool 추가. capability를 직접 정의해야 메서드의 effect가
알려지므로 외부 타입을 하나도 쓰지 않는다. 통과해야 하는 6개와 거부해야
하는 7개가 모두 의도대로 갈린다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 02:50:33 +09:00
61 changed files with 8417 additions and 314 deletions
+133
View File
@@ -0,0 +1,133 @@
# coollang
되돌리기 비싼 결정부터 확정하는 프로그래밍 언어. 현재 v0 — 설계 검증판.
```
$ coolc check samples/run/hello.cool
$ coolc run samples/run/hello.cool
area = 12
area = 9
area = 3
```
## 무엇을 위한 언어인가
세 가지 목표가 나머지 모든 결정을 지배한다.
1. **오류를 더 빨리 잡는다** — null 없음, Option/Result, 소진적 match,
확정 초기화, 선언되지 않은 effect는 컴파일 오류.
2. **검증이 더 빠르다** — fast path / slow path 분리, 전역 추론 없음,
복잡한 trait solver 없음, 임의 매크로 없음, 인터페이스 해시 기반 무효화.
3. **피해 범위가 좁다** — 명시적 capability, ambient authority 없음,
affine 소유권.
설계의 전문은 [`docs/thesis.md`](docs/thesis.md), 문법은
[`docs/grammar.ebnf`](docs/grammar.ebnf)에 있다.
## 지금 되는 것
`lex → parse → 이름 해소 → 타입 검사 → effect/capability 검사 →
move/affinity 검사 → 소진성 검사 → interface 해시 → typed IR → 인터프리터`
여섯 종류의 오류를 한국어 진단으로 보고한다: 문법, 이름, 타입, effect,
capability, 소유권. 여기에 match 소진성과 두 가지 lint(미사용 import,
effect 과잉 선언)가 더해진다.
```
$ coolc check samples/12_stdlib_effects.cool
samples/12_stdlib_effects.cool:20:14: 선언되지 않은 effect Console.print
(leaks_effect의 effects 절은 {}입니다)
```
## 증분 검사
이 프로젝트의 중심 주장이다. 10만 줄 / 200 모듈에서 측정 (`bench/bench.ml`):
| | 시간 | 재검사한 모듈 |
|---|---|---|
| 전체 검사 (cold) | 253ms | 200 |
| 함수 **본문**만 수정 | 0.8ms | **1** |
| 함수 **시그니처** 수정 | 2.5ms | **2** |
핵심은 시간이 아니라 범위다. 본문 수정이 downstream을 한 칸도 건드리지
않는 것, 그리고 시그니처 수정이 사슬 끝까지 가지 않고 해시가 변한 곳에서
멈추는 것 — 이 둘이 아키텍처의 주장 전부다.
시그니처 수정 시나리오에서는 두 모듈 건너의 `match`가 실제로 깨진다:
```
m100.cool:15:3: match가 모든 경우를 덮지 않습니다 (빠진 경우: 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이 필요하다.
```
opam install dune
dune build
dune test # 175개 검사
dune exec bench/bench.exe
```
`coolc``_build/default/bin/main.exe`다. 설치하려면 `dune install`.
## 저장소 구성
```
lib/ 컴파일러 (약 4,900줄 OCaml)
lexer.ml 어휘 분석 — 문법을 하나도 모른다
parser.ml 재귀 하강, LL(1), backtracking 없음
resolve.ml 이름 해소 — 모듈 하나만 보고 결정할 수 있는 것
typecheck.ml 타입 + effect + capability
move.ml move / affinity
exhaust.ml 소진성 (Maranget usefulness)
iface.ml interface artifact + 해시
session.ml 모듈 로딩 + 고정점 invalidation
ir.ml 얇은 typed IR
interp.ml 트리 워킹 인터프리터
std/ 표준 라이브러리 (본문 없는 선언, 런타임이 구현)
samples/ 예제 — 통과용 9개, 일부러 틀린 것 7개
bench/ 증분 루프 측정
```
## v0의 성격
이것은 쓸 수 있는 언어가 아니라 **설계가 옳은지 증명된 언어**다.
되돌리기 비싼 결정 — 문법, 타입, effect, capability, 소유권, 증분
아키텍처 — 이 전부 코드와 테스트로 못 박혔고, 빠른 검증 루프라는 시스템
속성이 측정으로 증명됐다.
그래서 이 코드를 통째로 버리고 v1로 번역해도 잃을 것이 없다. 애초에
그것이 v0의 목적이었다.
아직 없는 것: 진짜 컴파일(해석 실행만 한다), 병렬 실행(`scope`는 순차),
파일·네트워크 IO, 완전한 제네릭, 에디터 지원, 패키지 관리자. 표준
라이브러리는 `len`, `each`, `map`, `concat`, `show` 수준이다.
## 이름
언어는 coollang, 툴체인 바이너리는 `coolc`. 소스 확장자는 `.cool`.
+128
View File
@@ -0,0 +1,128 @@
(* 빠른 검증 루프는 주장이 아니라 측정이다.
측정하는 것은 두 가지다:
1. 전체 검사 시간 (cold) — 규모가 커져도 파국이 아닌가
2. 증분 재검사 시간 (warm) — 그리고 무엇이 재검사되었는가
두 번째가 본체다. 아키텍처의 주장은 "빠르다"가 아니라 "다시 볼 것이
적다"이고, 그것은 시간이 아니라 재검사된 모듈 수로 먼저 증명된다.
시간은 그 수가 옳다는 것의 따름 결과다. *)
open Coollang
let modules = 200
let fns_per_module = 68
let now () = Unix.gettimeofday ()
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 failwith "not found"
else if String.sub hay i n = needle then i
else go (i + 1)
in
go 0
let write file s =
let oc = open_out_bin file in
output_string oc s;
close_out oc
(* i번 모듈은 i-1번 모듈을 가져온다. 사슬이므로 시그니처 변경은 끝까지
전파되어야 하고, 본문 변경은 한 칸도 가면 안 된다. *)
let gen_module i ~body =
let b = Buffer.create 8192 in
if i > 0 then
Buffer.add_string b (Printf.sprintf "import \"m%d\" as Up\n\n" (i - 1));
Buffer.add_string b "pub enum Shape {\n Circle(Int),\n Square(Int),\n}\n\n";
Buffer.add_string b "pub copyable struct Point {\n x: Int,\n y: Int,\n}\n\n";
Buffer.add_string b
(Printf.sprintf
"pub fn area(s: Shape) -> Int {\n\
\ match s {\n\
\ Circle(r) => r * r,\n\
\ Square(w) => w * w,\n\
\ }\n\
}\n\n");
if i > 0 then
Buffer.add_string b
"pub fn up_area(s: Up.Shape) -> Int {\n\
\ match s {\n\
\ Up.Circle(r) => Up.area(s),\n\
\ Up.Square(w) => w * w,\n\
\ }\n\
}\n\n";
for k = 0 to fns_per_module - 1 do
Buffer.add_string b
(Printf.sprintf
"pub fn f%d(p: Point, n: Int) -> Int {\n\
\ let a = p.x + n\n\
\ let b = p.y * %d\n\
\ let c = if a > b { a } else { b }\n\
\ %s\n\
}\n\n"
k (k + 1) body)
done;
Buffer.contents b
let count_lines s =
String.fold_left (fun n c -> if c = '\n' then n + 1 else n) 0 s
let ms t = Printf.sprintf "%.1fms" (t *. 1000.)
let () =
let dir = Filename.concat (Filename.get_temp_dir_name ()) "cool_bench" in
ignore
(Sys.command
(Printf.sprintf "rm -rf %s && mkdir -p %s" (Filename.quote dir)
(Filename.quote dir)));
let path i = Filename.concat dir (Printf.sprintf "m%d.cool" i) in
let lines = ref 0 in
for i = 0 to modules - 1 do
let s = gen_module i ~body:"a + c" in
lines := !lines + count_lines s;
write (path i) s
done;
Printf.printf "모듈 %d개, %d줄 생성\n" modules !lines;
(* 1. cold: 전체 그래프 로드와 검사 *)
let st = Session.create ~root:dir () in
let t0 = now () in
Session.load st (path (modules - 1));
let cold = now () -. t0 in
let errs = Session.errors st in
Printf.printf "cold 전체 검사 %s (오류 %d건)\n" (ms cold) (List.length errs);
if errs <> [] then
List.iter
(fun e -> prerr_endline (Session.string_of_error e))
(List.filteri (fun i _ -> i < 5) errs);
(* 2. warm: 사슬 한가운데 모듈의 본문만 수정 *)
let mid = modules / 2 in
write (path mid) (gen_module mid ~body:"a + c + 1");
let t0 = now () in
let touched = Session.recheck st [ path mid ] in
let warm_body = now () -. t0 in
Printf.printf "warm 본문만 수정 %s (재검사 %d개, 오류 %d건)\n" (ms warm_body)
(List.length touched)
(List.length (Session.errors st));
(* 3. warm: 같은 모듈의 시그니처 수정 — variant 추가 *)
let sig_src =
let s = gen_module mid ~body:"a + c + 1" in
let needle = " Square(Int),\n" in
let i = find_sub s needle in
String.sub s 0 (i + String.length needle)
^ " Tri(Int),\n"
^ String.sub s
(i + String.length needle)
(String.length s - i - String.length needle)
in
write (path mid) sig_src;
let t0 = now () in
let touched = Session.recheck st [ path mid ] in
let warm_sig = now () -. t0 in
let errs = Session.errors st in
Printf.printf "warm 시그니처 수정 %s (재검사 %d개, 오류 %d건)\n" (ms warm_sig)
(List.length touched) (List.length errs);
List.iter (fun e -> print_endline (" " ^ Session.string_of_error e)) errs
+3
View File
@@ -0,0 +1,3 @@
(executable
(name bench)
(libraries coollang unix))
+1 -1
View File
@@ -1,4 +1,4 @@
(executable
(name main)
(public_name cool)
(public_name coolc)
(libraries coollang))
+78 -8
View File
@@ -2,12 +2,14 @@ let usage =
{|coollang toolchain
사용법:
cool check <file.cool>... 타입/effect/capability 검사 (fast path)
cool run <file.cool> typed IR 인터프리터로 실행
cool tokens <file.cool> 토큰 덤프 (렉서 디버깅)
cool ast <file.cool> 구문 트리 덤프 (파서 디버깅)
cool deps <file.cool> 외부 참조 목록 (모듈의 의존 표면)
cool version 버전 출력
coolc check <file.cool>... 타입/effect/capability 검사 (import를 따라 모듈 그래프 전체)
coolc test <file.cool> [필터] 모듈 그래프의 test 블록 실행
coolc iface <file.cool> interface 표면과 해시 출력
coolc run <file.cool> [인자...] typed IR 인터프리터로 실행
coolc tokens <file.cool> 토큰 덤프 (렉서 디버깅)
coolc ast <file.cool> 구문 트리 덤프 (파서 디버깅)
coolc deps <file.cool> 외부 참조 목록 (모듈의 의존 표면)
coolc version 버전 출력
|}
let report_errors errors =
@@ -32,6 +34,33 @@ let dump_ast file =
m.Coollang.Ast.items;
0
(* import를 따라 모듈 그래프를 로드하고 전부 검사한다. root는 첫 파일의 디렉터리다. *)
let check_graph files =
match files with
| [] ->
prerr_endline "검사할 파일이 없습니다";
2
| first :: _ ->
let st = Coollang.Session.create ~root:(Filename.dirname first) () in
List.iter (fun f -> Coollang.Session.load st f) files;
let errors = Coollang.Session.errors st in
List.iter
(fun e -> prerr_endline (Coollang.Session.string_of_error e))
errors;
if errors = [] then 0 else 1
let dump_iface file =
let st = Coollang.Session.create ~root:(Filename.dirname file) () in
Coollang.Session.load st file;
match Coollang.Session.find st file with
| None -> 1
| Some e ->
Printf.printf "hash %s\n" e.iface.Coollang.Iface.hash;
List.iter
(fun it -> print_endline (Coollang.Ast.show_item it))
e.iface.Coollang.Iface.items;
0
let dump_deps file =
match Coollang.Driver.resolve file with
| Error errors -> report_errors errors
@@ -46,8 +75,49 @@ let () =
let argv = Array.to_list Sys.argv in
let code =
match List.tl argv with
| "check" :: files -> report (Coollang.Driver.check files)
| [ "run"; file ] -> report (Coollang.Driver.run file)
| "check" :: files -> check_graph files
| [ "iface"; file ] -> dump_iface file
| "test" :: file :: rest -> (
let filter = match rest with f :: _ -> f | [] -> "" in
let st = Coollang.Session.create ~root:(Filename.dirname file) () in
match Coollang.Session.test ~filter st file with
| 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;
0
| out, Some e ->
(* 실패해도 그때까지의 출력을 먼저 보여준다 *)
print_string out;
flush stdout;
prerr_endline (Coollang.Session.string_of_error e);
1)
| [ "tokens"; file ] -> dump_tokens file
| [ "ast"; file ] -> dump_ast file
| [ "deps"; file ] -> dump_deps 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의 질문은 "되돌리기 비싼 결정이 옳은가"였다. 답은 **그렇다**이고,
남은 불편은 전부 되돌리기 싼 것들이다.
+177 -82
View File
@@ -7,10 +7,24 @@
* { } (0 )
* ( )
* " "
* < > ( )
* (* *)
*
* : LL(1). backtracking 없음, .
* ( 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 :
* ident, , ")", "]", "}", "?", "return"
* 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" | "affine" |
* "copyable" | "effects" | "return" | "if" | "else" | "match" |
* "scope" | "crash" | "test" | "true" | "false"
(* *)
*
* (, , , variant, )
* . NEWLINE을 만들지 않으므로 목록이 자연히 이어진다.
* formatter가 이를 강제한다.
*
* (struct , enum , struct , effect , match )
* NEWLINE .
* . { NEWLINE } .
*
* NEWLINE이 문법적으로 허용되고 무시된다. effects 절이
* "}" NEWLINE이 삽입되는데,
* { NEWLINE } .
* [ NEWLINE ] .
* .
*)
ident = letter , { letter | digit | "_" } ;
int_lit = digit , { digit | "_" } ;
string_lit = '"' , { char - '"' } , '"' ;
string_lit = '"' , { str_char } , '"' ;
str_char = ( char - '"' - "\" ) | escape ;
escape = "\" , ( "n" | "t" | "\" | '"' ) ;
bool_lit = "true" | "false" ;
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 } ;
item = ( import | reexport | decl ) , { NEWLINE } ;
(* decl NEWLINE 0 .
* stmt NEWLINE *)
module = [ NEWLINE ] , { item } ;
item = ( import | reexport | test_decl | decl ) , [ NEWLINE ] ;
import = "import" , string_lit , "as" , 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
| capability_decl | const_decl ) ;
@@ -65,53 +120,47 @@ decl = [ "pub" ] , ( fn_decl | struct_decl | enum_decl
(* ------------------------------------------------------------------ *)
fn_decl = "fn" , ident , [ gen_params ] , "(" , [ params ] , ")" ,
{ NEWLINE } ,
[ eff_result , { NEWLINE } ] ,
[ "->" , type , { NEWLINE } ] ,
[ NEWLINE ] ,
[ eff_result , [ NEWLINE ] ] ,
[ "->" , type , [ NEWLINE ] ] ,
[ block ] ;
(* block . interface capability *)
(* block . std/*.cool capability *)
struct_decl = [ "copyable" ] , "struct" , ident , [ gen_params ] ,
"{" , { field } , "}" ;
field = ident , ":" , type , { NEWLINE } ,
[ "," , { NEWLINE } ] ;
"{" , brace_list<field> , "}" ;
field = ident , ":" , type ;
enum_decl = "enum" , ident , [ gen_params ] , "{" , { variant } , "}" ;
variant = ident , [ "(" , type_list , ")" ] , { NEWLINE } ,
[ "," , { NEWLINE } ] ;
enum_decl = "enum" , ident , [ gen_params ] ,
"{" , brace_list<variant> , "}" ;
variant = ident , [ "(" , type_list , ")" ] ;
capability_decl = "capability" , ident , "{" , { NEWLINE } , { cap_method } , "}" ;
cap_method = "fn" , ident , "(" , [ params ] , ")" , { NEWLINE } ,
[ eff_result , { NEWLINE } ] , [ "->" , type ] ,
{ NEWLINE } ;
capability_decl = "capability" , ident , "{" , [ NEWLINE ] , { cap_method } , "}" ;
cap_method = "fn" , ident , [ gen_params ] , "(" , [ params ] , ")" ,
[ NEWLINE ] ,
[ eff_result , [ NEWLINE ] ] ,
[ "->" , type ] ,
[ NEWLINE ] ;
const_decl = "const" , ident , ":" , type , "=" , expr ;
gen_params = "[" , gen_param , { "," , gen_param } , [ "," ] , "]" ;
gen_params = "[" , list<gen_param> , "]" ;
gen_param = ident , [ ":" , "effects" ] ;
(* ident = , ": effects" = effect *)
params = param , { "," , param } , [ "," ] ;
params = list<param> ;
param = [ "own" ] , [ "mut" ] , ident , ":" , type ;
(* = use(). own .
* : (own, mut) , (affine) type *)
(* ------------------------------------------------------------------ *)
(* effect *)
(* effect *)
(* ------------------------------------------------------------------ *)
(* effect. *)
eff_result = "effects" , eff_union ;
eff_union = eff_atom , { "|" , eff_atom } ;
(* *)
eff_result = "effects" , eff_atom , { "|" , eff_atom } ;
(* effect. .
* " " " " *)
(* . .
* " " " " *)
eff_param = "effects" , eff_atom ;
eff_atom = ident | eff_set ;
eff_set = "{" , { NEWLINE } ,
[ eff_name , { { NEWLINE } , "," , { NEWLINE } , eff_name } ,
{ NEWLINE } , [ "," , { NEWLINE } ] ] , "}" ;
eff_set = "{" , brace_list<eff_name> , "}" ;
eff_name = ident , "." , ident ;
(* . capability identity *)
@@ -121,31 +170,41 @@ eff_name = ident , "." , ident ;
type = fn_type | named_type ;
fn_type = [ "affine" ] , "fn" , "(" , [ type_list ] , ")" ,
(* own . "
* " , *)
fn_type = [ "affine" ] , "fn" , "(" , [ list<fn_param_ty> ] , ")" ,
[ eff_param ] , [ "->" , type ] ;
fn_param_ty = [ "own" ] , type ;
named_type = ident , [ type_args ] ;
type_args = "[" , targ , { "," , targ } , [ "," ] , "]" ;
(* : Shapes.Shape.
* *)
named_type = ident , [ "." , ident ] , [ type_args ] ;
type_args = "[" , list<targ> , "]" ;
targ = type | eff_set ;
(* effect.
* *)
type_list = type , { "," , type } , [ "," ] ;
type_list = list<type> ;
(* ------------------------------------------------------------------ *)
(* *)
(* ------------------------------------------------------------------ *)
block = "{" , { NEWLINE } ,
[ stmt , { stmt_sep , stmt } , [ stmt_sep ] ] , "}" ;
stmt = let_stmt | return_stmt | assign_stmt | expr ;
stmt_sep = NEWLINE , { NEWLINE } ;
(* "}" .
block = "{" , [ NEWLINE ] , [ stmt , stmt_rest ] , "}" ;
stmt_rest = [ NEWLINE , [ stmt , stmt_rest ] ] ;
(* NEWLINE .
* , "-" "("
* .
* "}" .
* fn(s) { String.concat(prefix, s) } *)
stmt = let_stmt | return_stmt | expr_stmt ;
let_stmt = "let" , [ "mut" ] , pattern , [ ":" , type ] , "=" , expr ;
return_stmt = "return" , [ expr ] ;
assign_stmt = place , "=" , expr ;
place = ident , { "." , ident } ;
(* NEWLINE "}" *)
expr_stmt = expr , [ "=" , expr ] ;
(* ( )
* . ident *)
(* = stmt expr , Unit.
* return , return formatter *)
@@ -154,62 +213,98 @@ place = ident , { "." , ident } ;
(* *)
(* ------------------------------------------------------------------ *)
expr = or_expr ;
or_expr = and_expr , { "||" , and_expr } ;
and_expr = cmp_expr , { "&&" , cmp_expr } ;
cmp_expr = add_expr , [ cmp_op , add_expr ] ;
cmp_op = "==" | "!=" | "<" | "<=" | ">" | ">=" ;
add_expr = mul_expr , { ( "+" | "-" ) , mul_expr } ;
mul_expr = unary , { ( "*" | "/" | "%" ) , unary } ;
unary = [ "!" | "-" ] , postfix ;
(* s struct .
* expr = ( )
* expr_ns = (if/match )
* if/match "{" struct
* , struct .
* , , ,
* . primary<no>
* 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 ] , ")" ;
field_sfx = "." , ident ;
inst_sfx = type_args ;
(* "[" = , "[" = . *)
args = expr , { "," , expr } , [ "," ] ;
args = list<expr> ;
primary = literal
| ident
primary<s> = literal
| list_lit
| struct_lit
| closure
| if_expr
| match_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 . "{"
* *)
name_or_struct<yes> = ident , [ struct_body ] ;
name_or_struct<no> = ident ;
struct_body = "{" , brace_list<field_init> , "}" ;
field_init = ident , ":" , expr ;
list_lit = "[" , [ args ] , "]" ;
(* : let xs: List[Int] = [] *)
struct_lit = ident , "{" , { field_init } , "}" ;
field_init = ident , ":" , expr , { NEWLINE } , [ "," , { NEWLINE } ] ;
closure = "fn" , "(" , [ cl_params ] , ")" ,
[ eff_param ] , [ "->" , type ] , block ;
cl_params = cl_param , { "," , cl_param } , [ "," ] ;
cl_param = ident , [ ":" , type ] ;
cl_params = list<cl_param> ;
(* .
* move
* , .
* ( ) *)
cl_param = [ "own" ] , ident , [ ":" , type ] ;
(* . .
* error *)
if_expr = "if" , expr_ns , block , [ "else" , ( block | if_expr ) ] ;
match_expr = "match" , expr_ns , "{" , { arm } , "}" ;
arm = pattern , "=>" , ( expr | block ) , { NEWLINE } ,
[ "," , { NEWLINE } ] ;
match_expr = "match" , expr_ns , "{" , brace_list<arm> , "}" ;
arm = pattern , "=>" , ( expr | block ) ;
scope_expr = "scope" , ident , "=" , ident , block ;
(* scope = { ... }
* . " "
* ambient authority *)
(* expr_ns = struct_lit expr.
* if/match/scope "{" struct
* , struct *)
(* ------------------------------------------------------------------ *)
(* *)
(* ------------------------------------------------------------------ *)
pattern = "_" | literal | ctor_pattern | ident ;
ctor_pattern = ident , "(" , pattern , { "," , pattern } , [ "," ] , ")" ;
pattern = "_" | literal | name_pattern ;
(* . :
* (Shapes.Dot)
*
* , *)
name_pattern = ident , [ "." , ident ] , [ "(" , [ list<pattern> ] , ")" ] ;
(* . (exhaustiveness ) *)
+228 -4
View File
@@ -8,6 +8,11 @@ AI의 코드 생성 속도 >> 신뢰 확보 속도.
1. 오류는 더 일찍: 컴파일 타임으로 최대한 끌어당김
→ null 없음(Option), Result, exhaustive matching, definite init,
미선언 effect = compile error
※ definite init은 문법이 이미 보장한다: let은 항상 초기화식을 요구하고
미초기화 바인딩을 쓸 방법이 없다. 별도 검사가 필요 없는 것이 맞다
※ exhaustive matching은 interface hash가 enum 정의 본문을 입력으로 삼는
이유이기도 하다. upstream에 variant가 하나 늘면 downstream의 match가
깨져야 하는데, 이 검사가 없으면 깨질 것이 없다
2. 검증은 더 빨리: 검증 속도가 언어 설계의 헌법
→ fast path(check) / slow path(release, deep verify) 분리
→ 컴파일을 느리게/비결정적으로 만드는 기능 원천 배제
@@ -23,6 +28,16 @@ AI의 코드 생성 속도 >> 신뢰 확보 속도.
→ "Effects: +payment.refund" 수준으로 리뷰 압축
5. 한 개념 = 한 방식: syntax variant 최소화, 공식 formatter 하나,
공식 toolchain 하나. 단순성의 기준은 "작성자"가 아니라 "리뷰어와 검증기"
6. 이름은 관례가 아니라 뜻에서 고른다 — 낯섦은 한 번 치르고 끝나지만
부정확함은 읽는 사람마다 매번 치른다
→ 판정: 그 단어로 그것이 무엇인지 평범한 문장을 써 보라. 단어가 문장을
도우면 맞는 이름, 문장과 싸우면 틀린 이름이다
"크래시는 복구하는 것이 아니라 조사하는 것이다" — 돕는다
"패닉은 복구할 수 없다" — 다른 언어에서는 할 수 있어 싸운다
→ 널리 쓰인다는 것은 옳다는 증거가 아니다. 운영체제의 "fault"가 그렇다 —
잘못이 아닌 것을 잘못이라 부르고, 그 오류까지 함께 전파된다
※ 관례가 정확하면 관례를 따른다. 관례라서가 아니라 정확해서다
※ 이름에만 적용된다. 동작을 관례와 다르게 만들 이유는 아니다
■ 파생 결정
@@ -45,6 +60,13 @@ Alias/Move 모델 (철학 1,3에서 파생 — 언어 전체의 토대):
미해제는 검사하지 않는다 (오용 금지, 누수 허용).
linear 검사와 해제 보장은 v1 과제
- v0에 first-class reference는 없다. mutable 데이터는 소유 변수를 통해서만 변경
- 클로저는 mut 바인딩을 capture할 수 없다. 참조가 없으므로 별칭을 만들 수도,
조용히 복사할 수도 없기 때문이다. spawn 클로저 제한은 이 일반 규칙의 특수 사례다
- v0에 부분 move는 없다. 필드 접근은 빌림이고 결과도 빌린 값이다
※ struct에서 affine 필드만 꺼내 가려면 부분 move 상태 추적이 필요한데,
그 복잡도는 v0가 사려는 것이 아니다. 필요하면 통째로 own으로 받는다
- 외부 타입은 affine임을 증명할 수 없으므로 copyable로 본다.
모르는 것을 위반이라고 말하지 않는다 — 모듈 로딩이 생기면 판정된다
- 공유는 deep immutable 값만 가능 (내부 가변성 타입은 v0에 없음)
근거: "safe code에 data race 없음"은 spawn만 막아서 성립하지 않는다. closure,
channel, container, 인자/반환 전 경로에서 mutable alias가 없어야 하며, 값 의미론
@@ -107,8 +129,8 @@ Affinity 전이 (보안 주장의 필수 전제):
- structured concurrency만 허용: 태스크 수명 = 블록 구조 (locality)
- 데이터 경쟁은 격리로: mutable은 단일 소유, channel로 소유권 이동,
공유는 deep immutable만 (borrow checker는 complexity budget 초과)
- spawn closure는 by-move capture 또는 immutable capture만 허용.
mutable 참조 capture는 문법적으로 금지
- spawn closure는 by-move capture 또는 immutable capture만 허용
(Alias/Move 모델의 mut capture 금지가 그대로 적용된다)
- spawn은 primitive가 아니라 TaskScope capability의 메서드다.
effect spawn은 아래 "정적/동적 층 분리"대로 그 타입에 묶인다
(PaymentGateway.refund와 동형)
@@ -268,12 +290,23 @@ Generics (철학 2에서 파생):
※ 각 단계는 그 단계가 소유한 성질만 판정한다. 예: affinity는 타입 동등성이
아니라 substructural 성질이므로 타입 검사가 아니라 move 검사가 소유한다.
단계가 서로의 결론을 앞지르면 진단이 엉뚱한 곳에서 난다
※ 다만 effect 검사는 타입 검사와 같은 순회에서 돈다. effect 변수의 해소가
타입 변수와 같은 지점(호출 지점의 지역 unification)에서 일어나므로,
떼어내면 순회와 인스턴스화를 두 번 하게 된다. 소유는 나뉘되 순회는 하나다
※ v0는 과잉 선언(선언했으나 수행하지 않는 effect)을 오류로 보지 않는다.
외부 모듈의 effect를 모르는 상태에서는 판정할 수 없기 때문이다.
모듈 로딩이 생기면 lint 대상이다
- move/affinity 검사, capability use 규칙, affinity 전이
- effect 변수 (effect 다형성)
- interface artifact + hash 기반 incremental invalidation
- cool check
- 얇은 typed IR + tree-walking interpreter (cool run 대용)
- coolc check
- 얇은 typed IR + tree-walking interpreter (coolc run)
※ IR을 미루면 non-IR 전제가 스며들어 재작성 됨. 지금, 얇게.
※ 실행 시점에도 권한의 출처는 런타임 하나다. main이 선언한 capability만
넘어가고, 소스에는 capability를 만드는 문법이 없다 — 보안 정리 (i)의
실행 시점 대응물. TaskScope의 뿌리도 같은 이유로 런타임이 준다.
※ v0의 scope 실행 의미는 순차다. 구조가 먼저고 병렬성은 그 위의 최적화다 —
순서가 반대면 취소와 전파를 나중에 끼워 넣게 된다.
※ effect check가 fast path 예산 안에 드는지가 사활 → 최우선 검증 대상
제외 (아키텍처 검증 후 얹어도 되는 것):
@@ -314,3 +347,194 @@ L2 빠른 테스트 / L3 fuzzing / L4 formal proof → 요청 시, 분리 실행
오류를 더 빨리 잡는가? / 컴파일 복잡도·시간은 예측 가능한가? /
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 (오류다, 경고가 아니다)
- 미사용 import: 쓰지 않는 모듈의 시그니처가 바뀌면 이 모듈이 재검사된다.
증분 루프의 비용을 이유 없이 넓히는 선언은 남겨둘 수 없다.
- effect 과잉 선언: 선언하고 수행하지 않으면 호출자가 하지도 않는 일에
의무를 진다. 시그니처는 실제보다 좁아도 안 되고 넓어도 안 된다.
단, effect 변수가 있거나 본문에 모르는 이름이 있으면 판정하지 않는다 —
무엇이 묶일지는 호출 지점이 정하고, 외부 타입의 메서드는 effect를 모른다.
lint는 blocking이 아니다. 이름 해소 실패는 뒤 단계를 막지만 lint는 막지
않는다 — lint 하나가 진짜 타입 오류를 가리면 루프가 느려진다.
■ 파서 오류 복구
항목 단위로만 회복한다. 오류가 난 선언은 통째로 버리고 다음 선언에서
다시 시작한다. 문 단위로 더 잘게 회복하려 하면 파서가 추측을 하게 되고,
틀린 추측은 없는 오류를 지어낸다. 한 항목에 오류 하나가 상한이라는 것은
정직한 한계이지 숨길 것이 아니다.
동기화 지점: 중괄호 깊이 0 + 줄 첫머리 + 선언 시작 토큰. 셋 다 필요하다.
■ 표준 라이브러리 (std/)
본문 없는 선언 파일이다. 런타임이 구현하고 .cool 파일은 계약만 말한다.
구현이 아니라 시험대인 것이 요점 — effect 다형성이 실제로 쓸 만한지가
List.each와 List.map에서 결정된다. 규칙이 틀렸으면 여기서 드러난다.
prelude는 없다. std도 명시적으로 가져온다 — 암묵적으로 끌어오지 않는다는
규칙에 예외를 두지 않는다.
※ std를 넣자마자 샘플 01이 깨졌다. List.each에 Result를 반환하는 클로저를
넘기고 그 안에서 ?를 쓰고 있었다. 검사되지 않던 코드가 검사되기 시작한
것이고, 이것이 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. 그 구멍이 std의 fold가 틀린 것을
덮고 있었다 — 누적자는 매 단계 소비되므로 own이다.
LRU 캐시가 제약 하나를 더했다: 축출은 렉시컬이 아니므로 with가 못 닿는다.
따라서 자원은 렉시컬 전용으로 간다 — 자원을 담는 컨테이너(풀, 캐시,
레지스트리)는 런타임만 만들 수 있고, 사용자는 언어로 못 만든다. 그 대가를
알고 택한다. 이미 그렇게 되어 있었다 — std-draft의 Pool이 불투명한 런타임
capability인 이유가 만들 수 없어서였다.
그리고 큰 버그 하나를 잡았다: Option/Result의 패턴이 타입을 통째로 잃고
있었다. 내장 생성자가 typecheck에 등록되지 않아 None이 변수 바인딩이 되고
Ok(v)의 v가 TUnknown이었다. Ok(v) => v 로 Int를 String 자리에 두는 코드가
통과했다. 소진성 검사는 자기 목록에 내장을 갖고 있어 이 사실을 덮고 있었다.
전문은 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)
100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현.
cold 전체 검사 245ms
본문만 수정 0.8ms, 재검사 1개 모듈
시그니처 수정 2.1ms, 재검사 2개 모듈 (+ downstream 소진성 위반 검출)
증분 루프의 비용은 시간이 아니라 재검사 범위가 결정한다. 본문 수정이
downstream을 한 칸도 건드리지 않는 것이 이 아키텍처의 주장이고, 위 수치의
"재검사 1개"가 그 주장이다. 시그니처 수정이 사슬 끝까지 가지 않고 2개에서
멈추는 것도 같은 규칙의 결과다 — m101의 interface는 변하지 않으므로 m102는
다시 볼 이유가 없다.
+455
View File
@@ -0,0 +1,455 @@
# 개밥 먹기 발견 기록
`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`의 메서드로 두고 핸들을 인자로 받는다:
```cool
fn close(own f: WriteFile) effects {Fs.close} -> Result[Unit, IoError]
```
`f.close()` 대신 `fs.close(f)`가 된다. 읽기에 나쁘지 않고 오히려 권한
(`Fs`)이 필요하다는 게 보인다. **지금은 우회로 충분해 보인다.**
### 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 안에 풀을 넣어 두면
되므로 구현상의 문제는 아니지만, **언어에 표현할 자리가 없다.**
### 잘 된 것 — 설계가 버틴 부분
- **두 층 자원이 자연스럽게 중첩된다.** 풀이 바깥, 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 절에 나타나 검사된다.
**순서**
1. **D4** — 수신자를 소비하는 메서드. 자원이 자기 정리를 스스로 선언할 수
있어야 한다
2. **`resource` 종류와 `with`** — 렉시컬 수명, 정리 강제, 전이 규칙
(자원을 필드로 가진 타입은 전이적으로 자원)
3. 그 뒤 Mini Shell로 세 번째 데이터 점
**LRU가 더한 제약**: 자원은 렉시컬 전용으로 간다. 자원을 담는 컨테이너는
런타임만 만들 수 있다 — 축출처럼 비렉시컬한 정리는 `with`가 못 닿기 때문이다.
사용자가 자기 자원 풀을 언어로 못 만드는 것이 그 대가다.
**미루는 것**: crash 시 정리 여부. `with`를 넣어도 이 질문은 열려 있다 —
정상 종료·`return`·`?`에서는 돌고 crash에서는 안 도는 것이 지금의 잠정
답이지만, 감독(supervision)이 들어오면 다시 봐야 한다.
+67
View File
@@ -0,0 +1,67 @@
# 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`가 통과한다고 그 프로그램이 옳게 도는 것은 아니다 — 돌려본 적이 없다.
## 세트
| | | 압박하는 곳 | 상태 |
|---|---|---|---|
| 1 | Mini Shell | 소유권 / OS 핸들 | 대기 |
| 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번을 먼저 둔 이유는
**동시성을 안 섞고 자원 모델만 시험할 수 있는 유일한 것**이기 때문이다.
발견은 `FINDINGS.md`에 쌓고, 확정된 결정은 `docs/thesis.md`로 올린다.
+58
View File
@@ -0,0 +1,58 @@
// 제대로 정리하는 버전.
//
// naive.cool과 같은 일을 하지만 모든 실패 경로에서 핸들을 닫고 임시 파일을
// 지운다. 두 파일의 차이가 이 개밥 먹기의 산출물이다.
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, Fs.write, Fs.sync, Fs.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 {Fs.write, Fs.sync, Fs.close, Fs.rename, Fs.remove}
-> Result[Unit, Fs.IoError] {
match fs.write(f, contents) {
Err(e) => abandon(fs, f, tmp, e),
Ok(_) => match fs.sync(f) {
Err(e) => abandon(fs, f, tmp, e),
Ok(_) => match fs.close(f) {
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 {Fs.close, Fs.remove} -> Result[Unit, Fs.IoError] {
let _ = fs.close(f)
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)
}
+18
View File
@@ -0,0 +1,18 @@
// 순진한 버전 — ?로 짧게 쓴 것.
//
// 이 파일의 목적은 통과하는 것이 아니라, 통과한다는 사실을 보여주는 것이다.
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, Fs.write, Fs.sync, Fs.close, Fs.rename}
-> Result[Unit, Fs.IoError] {
let tmp = String.concat(path, ".tmp")
let f = fs.create(tmp)?
fs.write(f, contents)?
fs.sync(f)?
fs.close(f)?
fs.rename(tmp, path)?
Ok(unit)
}
+58
View File
@@ -0,0 +1,58 @@
// 제대로 반납하는 버전.
//
// Atomic Updater와 비교하는 것이 목적이다. 자원이 두 층이고, 반납이
// 파괴가 아니며, 획득이 실패할 수 있다 — 파일에는 셋 다 없었다.
import "../std-draft/db" as Db
// 한 요청. 성공하든 실패하든 lease를 반납한다.
pub fn load_user(db: Db.Db, pool: Db.Pool, sql: String)
effects {Db.acquire, Db.query, Db.release} -> Result[List[String], Db.DbError] {
match db.acquire(pool) {
Err(e) => Err(e),
// l을 소비하는 인자와 l을 쓰는 인자를 한 호출에 섞을 수 없다.
// move 검사는 인자를 왼쪽부터 걷기 때문이다 — 실제 평가 순서와
// 무관하다. 먼저 질의하고 결과를 넘긴다.
Ok(l) => {
let r = db.query(l, sql)
release_then(db, pool, l, r)
},
}
}
// 반납하고 원래 결과를 그대로 돌려준다.
// 반납 실패는 일부러 버린다 — 원래 결과가 더 중요하다.
//
// 파일 때와 다른 점: 정리가 실패 경로마다 다르지 않다. 성공이든 실패든
// 같은 일(반납)을 하므로 결과를 통째로 실어나르는 도우미 하나로 접힌다.
pub fn release_then(
db: Db.Db,
pool: Db.Pool,
own l: Db.Lease,
r: Result[List[String], Db.DbError],
) effects {Db.release} -> Result[List[String], Db.DbError] {
let _ = db.release(pool, l)
r
}
// 프로그램 하나. 풀을 열고 쓰고 닫는다.
pub fn run(db: Db.Db, url: String, sql: String)
effects {Db.pool, Db.acquire, Db.query, Db.release, Db.close_pool}
-> Result[List[String], Db.DbError] {
match db.pool(url, 8) {
Err(e) => Err(e),
Ok(pool) => {
let rows = load_user(db, pool, sql)
close_then(db, pool, rows)
},
}
}
pub fn close_then(
db: Db.Db,
own pool: Db.Pool,
r: Result[List[String], Db.DbError],
) effects {Db.close_pool} -> Result[List[String], Db.DbError] {
let _ = db.close_pool(pool)
r
}
+25
View File
@@ -0,0 +1,25 @@
// 순진한 버전 — ?로 짧게 쓴 것.
//
// Atomic Updater와 같은 병이 나오는지 본다. 다만 자원이 두 층이다 —
// 풀과 lease. 그리고 반납은 파괴가 아니다.
import "../std-draft/db" as Db
// 한 요청. lease를 빌리고, 질의하고, 반납한다.
pub fn load_user(db: Db.Db, pool: Db.Pool, sql: String)
effects {Db.acquire, Db.query, Db.release} -> Result[List[String], Db.DbError] {
let l = db.acquire(pool)?
let rows = db.query(l, sql)? // 실패하면 lease가 반납되지 않는다
db.release(pool, l)?
Ok(rows)
}
// 프로그램 하나. 풀을 열고 쓰고 닫는다.
pub fn run(db: Db.Db, url: String, sql: String)
effects {Db.pool, Db.acquire, Db.query, Db.release, Db.close_pool}
-> Result[List[String], Db.DbError] {
let pool = db.pool(url, 8)?
let rows = load_user(db, pool, sql)? // 실패하면 풀도 안 닫힌다
db.close_pool(pool)?
Ok(rows)
}
+73
View File
@@ -0,0 +1,73 @@
// 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(db: Db.Db, pool: Db.Pool, own p: Put)
effects {Db.release} -> LeaseCache {
match p {
Kept(c) => c,
Evicted(c, l) => release_then(db, pool, l, c),
}
}
pub fn release_then(
db: Db.Db,
pool: Db.Pool,
own l: Db.Lease,
own c: LeaseCache,
) effects {Db.release} -> LeaseCache {
let _ = db.release(pool, l)
c
}
// 쓰는 쪽. 매 삽입마다 축출을 받아 처리해야 한다.
pub fn add(db: Db.Db, pool: Db.Pool, own c: LeaseCache, key: String)
effects {Db.acquire, Db.release} -> Result[LeaseCache, Db.DbError] {
match db.acquire(pool) {
Err(e) => Err(e),
Ok(l) => Ok(drain(db, pool, 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],
}
+51
View File
@@ -0,0 +1,51 @@
// 데이터베이스 연결 풀 — 개밥 먹기용 초안.
//
// 세 층이 있고 수명을 관리하는 주체가 각각 다르다.
// Conn 실제 연결. 풀이 소유한다. 사용자는 존재도 모른다
// Pool 연결 묶음. 프로그램 수명. 사용자가 만들고 닫는다
// Lease 빌린 한 자리. 요청 하나 수명. 반드시 반납해야 한다
//
// 사용자가 관리하는 것은 연결의 수명이 아니라 lease의 수명이다.
// 그래서 반납은 "파괴"가 아니다 — 연결은 풀로 돌아간다.
// ※ List[String]의 List는 내장 타입이라 import가 필요 없다. 모듈 List를
// 가져오는 것은 그 타입에 딸린 함수(len, map...)를 쓸 때뿐이다.
pub enum DbError {
// 연결 자체가 안 된다
Unreachable(String),
// 풀이 가득 찼고 기다릴 수 없다. 호출자가 대처할 수 있다 — 재시도든 포기든
Exhausted,
// 질의가 틀렸다. 프로그램의 결함에 가깝지만 런타임 값에서 오므로 Result다
BadQuery(String),
Other(String),
}
// 풀. capability이므로 affine이다 — 복제되지 않는다.
pub capability Pool {
}
// 빌린 한 자리. 반드시 반납해야 한다.
// ※ 언어는 아직 그 "반드시"를 강제하지 못한다 (D3).
pub capability Lease {
}
// 데이터베이스에 손댈 권한. 풀을 만들 수 있는 유일한 출처다.
pub capability Db {
// 풀을 연다. size는 최대 동시 연결 수.
fn pool(url: String, size: Int) effects {Db.pool} -> Result[Pool, DbError]
// 풀을 닫는다. 안에 있는 연결이 전부 정리된다.
// 아직 빌려나간 lease가 있으면? — 아래 발견 참고.
fn close_pool(own p: Pool) effects {Db.close_pool} -> Result[Unit, DbError]
// 한 자리를 빌린다. 없으면 Exhausted — 기다리지 않는다.
// 기다리는 형태(timeout)는 취소를 정해야 해서 미룬다.
fn acquire(p: Pool) effects {Db.acquire} -> Result[Lease, DbError]
// 반납한다. 연결은 풀로 돌아간다 — 파괴가 아니다.
fn release(p: Pool, own l: Lease) effects {Db.release} -> Result[Unit, DbError]
// 빌린 자리로 질의한다. lease는 빌린다.
fn query(l: Lease, sql: String) effects {Db.query} -> Result[List[String], DbError]
}
+65
View File
@@ -0,0 +1,65 @@
// 파일 시스템 — 개밥 먹기용 초안.
//
// 이 파일은 구현이 아니라 계약이다. 런타임이 구현한다고 가정하고, 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의 뿌리이므로 복제되지 않는다
// 메서드가 없다. 핸들로 할 수 있는 일은 Fs를 통해서 한다 — 아래 참고.
pub capability WriteFile {
}
// 파일 시스템에 손댈 권한.
//
// 핸들의 메서드가 아니라 Fs의 메서드로 둔 이유: close가 핸들을 소비해야
// 하는데, capability 메서드는 수신자를 소비할 방법이 없다. own은 파라미터에만
// 붙는다. 그래서 핸들을 인자로 받는 형태가 된다.
// ※ 발견 1: capability 메서드가 수신자를 소비할 수 없다.
pub capability Fs {
// 새로 만든다. 이미 있으면 자른다.
fn create(path: String) effects {Fs.create} -> Result[WriteFile, IoError]
// 핸들을 빌린다. 여러 번 쓸 수 있다.
fn write(f: WriteFile, s: String) effects {Fs.write} -> Result[Unit, IoError]
// 디스크까지 내려간다. 이게 없으면 rename이 원자적이어도 내용이 없을 수 있다.
fn sync(f: WriteFile) effects {Fs.sync} -> Result[Unit, IoError]
// 핸들을 소비한다. 두 번 닫을 수 없다 — own이 그것을 강제한다.
fn close(own f: WriteFile) effects {Fs.close} -> Result[Unit, 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]
}
+47 -10
View File
@@ -11,10 +11,18 @@ type eff_atom = Eff_var of string | Eff_set of eff_name list
type eff_result = eff_atom list (* 합집합. 길이 1이면 단일 *)
type ty =
| T_named of { name : string; args : targ list; pos : pos }
(* modl: 다른 모듈의 타입은 별칭으로 한정한다 (Shapes.Shape).
한정하지 않으면 이 모듈의 이름이다 — 암묵적으로 끌어오지 않는다. *)
| T_named of {
modl : string option;
name : string;
args : targ list;
pos : pos;
}
| T_fn of {
affine : bool;
params : ty list;
(* 파라미터마다 소유권 표시. 무표기는 빌림 *)
params : fn_param_ty list;
eff : eff_atom option;
ret : ty option;
pos : pos;
@@ -22,13 +30,19 @@ type ty =
(* 제네릭 인자는 타입 또는 effect다. 맨 이름은 둘 다일 수 있으므로
파서는 타입으로 읽고 이름 해소가 판정한다. *)
and fn_param_ty = { pt_own : bool; pt_ty : ty }
and targ = TA_ty of ty | TA_eff of eff_atom
type pattern =
| P_wild of pos
| P_lit of lit * pos
| P_bind of string * pos
| P_ctor of { name : string; args : pattern list; pos : pos }
| P_ctor of {
modl : string option;
name : string;
args : pattern list;
pos : pos;
}
type unop = U_not | U_neg
@@ -62,16 +76,21 @@ type expr =
| E_inst of { callee : expr; args : targ list; pos : pos }
| E_try of { inner : 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 }
and closure = {
cl_params : (string * ty option) list;
(* (own, 이름, 타입). 타입은 기대 타입에서 읽어오지만 소유권은 리터럴이
스스로 적는다 — move 검사가 타입을 모르기 때문이다 *)
cl_params : cl_param list;
cl_eff : eff_atom option;
cl_ret : ty option;
cl_body : block;
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 block = { stmts : stmt list; block_pos : pos }
@@ -136,6 +155,9 @@ type item =
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 }
@@ -164,7 +186,8 @@ let buf_eff_atom b = function
names
let rec buf_ty b = function
| T_named { name; args; _ } ->
| T_named { modl; name; args; _ } ->
let name = match modl with None -> name | Some m -> m ^ "." ^ name in
if args = [] then Buffer.add_string b name
else (
Buffer.add_string b ("(" ^ name);
@@ -176,7 +199,11 @@ let rec buf_ty b = function
Buffer.add_char b ')')
| T_fn { affine; params; eff; ret; _ } ->
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 ')';
(match eff with
| None -> ()
@@ -202,7 +229,8 @@ let rec buf_pattern b = function
| P_wild _ -> Buffer.add_char b '_'
| P_lit (l, _) -> buf_lit b l
| P_bind (n, _) -> Buffer.add_string b n
| P_ctor { name; args; _ } ->
| P_ctor { modl; name; args; _ } ->
let name = match modl with None -> name | Some m -> m ^ "." ^ name in
Buffer.add_string b ("(" ^ name);
List.iter
(fun p ->
@@ -230,6 +258,10 @@ let binop_name = function
let rec buf_expr b = function
| 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_list (xs, _) ->
Buffer.add_string b "(list";
@@ -251,9 +283,10 @@ let rec buf_expr b = function
| E_closure c ->
Buffer.add_string b "(closure (";
buf_list b
(fun (n, t) ->
Buffer.add_string b n;
match t with
(fun (p : cl_param) ->
if p.cp_own then Buffer.add_string b "own ";
Buffer.add_string b p.cp_name;
match p.cp_ty with
| None -> ()
| Some t ->
Buffer.add_char b ':';
@@ -454,6 +487,10 @@ let buf_item b = function
Buffer.add_char b ' ';
buf_expr b value;
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 b = Buffer.create 256 in
+25 -32
View File
@@ -1,8 +1,10 @@
(* v0 파이프라인.
parse -> name resolution -> type check -> effect/capability check
-> interface artifact + hash -> (cool run 시) 얇은 typed IR -> interpreter
-> interface artifact + hash -> (coolc run 시) 얇은 typed IR -> interpreter
현재 구현된 단계: 어휘 분석, 구문 분석. *)
이 파일은 단일 파일 도구(tokens/ast/deps)만 남았다. 모듈 그래프를 다루는
check와 run은 Session이 소유한다 — import를 따라가야 하는 순간부터
"파일 하나"는 더 이상 단위가 아니다. *)
type error = { file : string; line : int; col : int; message : string }
@@ -69,19 +71,27 @@ let typecheck (file : string) : (unit, error list) result =
{ file; line = e.pos.line; col = e.pos.col; message = e.msg })
rerrors)
| [] -> (
match Typecheck.check m with
let terrors =
List.map
(fun (e : Typecheck.error) ->
{ file; line = e.pos.line; col = e.pos.col; message = e.msg })
(Typecheck.check m)
in
(* move/affinity는 타입·effect와 달리 별도 순회다. 소유하는 성질이
다르고 해소를 공유할 지점도 없기 때문이다. *)
let merrors =
List.map
(fun (e : Move.error) ->
{ file; line = e.pos.line; col = e.pos.col; message = e.msg })
(Move.check m)
in
match
List.sort
(fun a b -> compare (a.line, a.col) (b.line, b.col))
(terrors @ merrors)
with
| [] -> Ok ()
| terrors ->
Error
(List.map
(fun (e : Typecheck.error) ->
{
file;
line = e.pos.line;
col = e.pos.col;
message = e.msg;
})
terrors)))
| errors -> Error errors))
let check (files : string list) : (unit, error list) result =
match files with
@@ -94,21 +104,4 @@ let check (files : string list) : (unit, error list) result =
files
|> List.concat
in
if errors <> [] then Error errors
else
(* 타입 검사는 통과했다. 통과했다고 말하지 않는다 — 파이프라인의
나머지가 아직 없으므로 검사되지 않은 것이다. *)
Error
(List.map
(fun f ->
{
file = f;
line = 0;
col = 0;
message =
"타입 검사까지 통과. effect/capability 검사와 move 검사가 아직 구현되지 않았습니다";
})
files)
let run (file : string) : (unit, error list) result =
Error [ { file; line = 0; col = 0; message = "interpreter가 아직 구현되지 않았습니다" } ]
if errors <> [] then Error errors else Ok ()
+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)
+203
View File
@@ -0,0 +1,203 @@
(* match exhaustiveness와 도달 불가 팔 검사 (Maranget의 usefulness 알고리즘).
철학 1의 대표 항목이다. 그리고 interface hash가 enum 정의 본문을 입력으로
삼는 이유이기도 하다 — upstream에 variant가 하나 늘면 downstream의 match가
깨져야 하는데, 이 검사가 없으면 깨질 것이 없다.
생성자 집합을 알 수 없는 타입(외부 타입, 미지수)은 검사하지 않는다.
모르는 것을 위반이라고 말하지 않는다. *)
module T = Types
(* 패턴을 검사용 형태로 줄인다. 바인딩은 와일드카드와 같다 —
무엇을 덮는가만 중요하다. *)
type cpat = CWild | CCtor of string * cpat list
type ctor = { c_name : string; c_args : T.t list }
type ctors = Finite of ctor list | Infinite
type env = {
(* enum 이름 -> variant 목록. 제네릭은 인스턴스화해서 넘어온다 *)
variants : string -> T.t list -> (string * T.t list) list option;
is_ctor : string -> bool;
}
let ctors_of env (t : T.t) : ctors =
match T.resolve t with
| T.TBool ->
Finite
[ { c_name = "true"; c_args = [] }; { c_name = "false"; c_args = [] } ]
| T.TUnit -> Finite [ { c_name = "unit"; c_args = [] } ]
| T.TCon ("Option", [ a ]) ->
Finite
[
{ c_name = "Some"; c_args = [ a ] }; { c_name = "None"; c_args = [] };
]
| T.TCon ("Result", [ a; b ]) ->
Finite
[
{ c_name = "Ok"; c_args = [ a ] }; { c_name = "Err"; c_args = [ b ] };
]
| T.TCon (n, args) -> (
match env.variants n args with
| Some vs ->
Finite
(List.map (fun (name, tys) -> { c_name = name; c_args = tys }) vs)
| None -> Infinite)
| _ -> Infinite
let rec of_pattern env (p : Ast.pattern) : cpat =
match p with
| Ast.P_wild _ -> CWild
| Ast.P_lit (Ast.L_bool true, _) -> CCtor ("true", [])
| Ast.P_lit (Ast.L_bool false, _) -> CCtor ("false", [])
(* 리터럴은 인자 없는 생성자다. 와일드카드로 바꾸면 모든 값을 덮는 것이 되어
Int 리터럴 몇 개로 exhaustive가 되어버린다. 타입의 생성자 집합이 무한하므로
리터럴만으로는 결코 완전해지지 않는다. *)
| Ast.P_lit (Ast.L_int n, _) -> CCtor ("<" ^ n ^ ">", [])
| Ast.P_lit (Ast.L_str v, _) -> CCtor ("<" ^ String.escaped v ^ ">", [])
| Ast.P_bind (n, _) -> if env.is_ctor n then CCtor (n, []) else CWild
| Ast.P_ctor { modl; name; args; _ } ->
let name = match modl with Some a -> a ^ "." ^ name | None -> name in
if env.is_ctor name then CCtor (name, List.map (of_pattern env) args)
else CWild
let wilds n = List.init n (fun _ -> CWild)
(* 행렬을 생성자 c로 특수화한다 *)
let specialize (c : ctor) (matrix : cpat list list) : cpat list list =
let arity = List.length c.c_args in
List.filter_map
(fun row ->
match row with
| CCtor (n, args) :: rest ->
if n = c.c_name then Some (args @ rest) else None
| CWild :: rest -> Some (wilds arity @ rest)
| [] -> None)
matrix
let default_matrix (matrix : cpat list list) : cpat list list =
List.filter_map
(fun row ->
match row with
| CCtor _ :: _ -> None
| CWild :: rest -> Some rest
| [] -> None)
matrix
let head_names (matrix : cpat list list) =
List.filter_map
(fun row -> match row with CCtor (n, _) :: _ -> Some n | _ -> None)
matrix
(* 행렬이 덮지 못하는 반례 벡터를 찾는다. None이면 완전하다. *)
let rec witness env (matrix : cpat list list) (tys : T.t list) :
cpat list option =
match tys with
| [] -> if matrix = [] then Some [] else None
| th :: rest -> (
let heads = head_names matrix in
match ctors_of env th with
| Finite cs
when List.for_all (fun c -> List.mem c.c_name heads) cs && cs <> [] ->
(* 모든 생성자가 나타났다: 각각으로 파고든다 *)
let rec try_each = function
| [] -> None
| c :: more -> (
let arity = List.length c.c_args in
match witness env (specialize c matrix) (c.c_args @ rest) with
| Some ws ->
let args = List.filteri (fun i _ -> i < arity) ws in
let tail = List.filteri (fun i _ -> i >= arity) ws in
Some (CCtor (c.c_name, args) :: tail)
| None -> try_each more)
in
try_each cs
| kind -> (
(* 빠진 생성자가 있거나 집합이 무한하다 *)
match witness env (default_matrix matrix) rest with
| None -> None
| Some ws ->
let head =
match kind with
| Finite cs -> (
match
List.find_opt (fun c -> not (List.mem c.c_name heads)) cs
with
| Some c -> CCtor (c.c_name, wilds (List.length c.c_args))
| None -> CWild)
| Infinite -> CWild
in
Some (head :: ws)))
let rec show_cpat = function
| CWild -> "_"
| CCtor (n, []) -> n
| CCtor (n, args) ->
n ^ "(" ^ String.concat ", " (List.map show_cpat args) ^ ")"
(* 행 q가 행렬 P에 대해 쓸모 있는가 = P가 덮지 못하는 값을 q가 덮는가 *)
let useful env (matrix : cpat list list) (q : cpat list) (tys : T.t list) : bool
=
let rec go matrix q tys =
match (q, tys) with
| [], [] -> matrix = []
| qh :: qt, th :: tt -> (
match qh with
| CCtor (n, args) -> (
match ctors_of env th with
| Finite cs -> (
match List.find_opt (fun c -> c.c_name = n) cs with
| Some c -> go (specialize c matrix) (args @ qt) (c.c_args @ tt)
| None -> go (default_matrix matrix) qt tt)
| Infinite ->
let c =
{ c_name = n; c_args = List.map (fun _ -> T.TUnknown) args }
in
go (specialize c matrix) (args @ qt) (c.c_args @ tt))
| CWild -> (
let heads = head_names matrix in
match ctors_of env th with
| Finite cs
when List.for_all (fun c -> List.mem c.c_name heads) cs
&& cs <> [] ->
List.exists
(fun c ->
go (specialize c matrix)
(wilds (List.length c.c_args) @ qt)
(c.c_args @ tt))
cs
| _ -> go (default_matrix matrix) qt tt))
| _ -> false
in
go matrix q tys
type result = {
missing : string option; (* 빠진 경우의 반례 *)
unreachable : int list; (* 도달할 수 없는 팔의 번호 (0부터) *)
}
let check env (scrutinee : T.t) (pats : Ast.pattern list) : result =
match ctors_of env scrutinee with
| Infinite when T.resolve scrutinee = T.TUnknown ->
(* 생성자 집합을 모르면 검사하지 않는다 *)
{ missing = None; unreachable = [] }
| _ ->
let rows = List.map (fun p -> [ of_pattern env p ]) pats in
let unreachable =
let acc = ref [] in
List.iteri
(fun i row ->
let before = List.filteri (fun j _ -> j < i) rows in
if not (useful env before row [ scrutinee ]) then acc := i :: !acc)
rows;
List.rev !acc
in
let missing =
(* 리터럴 패턴이 섞이면 정확한 반례를 만들 수 없다 — 그 열은 무한
집합이므로 와일드카드가 없으면 불완전으로 본다 *)
match witness env rows [ scrutinee ] with
| Some ws -> (
match ws with [ w ] -> Some (show_cpat w) | _ -> Some "_")
| None -> None
in
{ missing; unreachable }
+201
View File
@@ -0,0 +1,201 @@
(* interface artifact와 그 해시.
hash 입력 = 모듈 exported surface 전체의 의미적 정규형이다 (문서 P7).
함수 시그니처(effect 포함), 타입 정의 본문(struct 필드, enum variant),
타입의 affinity, 상수의 타입과 값, capability 선언, 그리고 reexport된
선언을 완전히 해소한 정의 본문.
원칙: downstream 검사 결과에 영향을 줄 수 있는 모든 것을 포함한다.
의심스러우면 넣는다 — 과잉 포함의 비용은 재검사지만 누락의 비용은
잘못된 캐시라는 비대칭이 있다.
함수 본문은 들어가지 않는다. 본문 한 줄 수정이 해시를 흔들면 incremental
전제가 무너진다. *)
open Ast
type t = { items : item list; (* 본문을 벗긴 exported surface *) hash : string }
let strip_body (d : fn_decl) = { d with fn_body = None }
let is_exported = function
| I_fn { pub; _ } -> pub
| I_struct { pub; _ } -> pub
| I_enum { pub; _ } -> pub
| I_capability { pub; _ } -> pub
| I_const { pub; _ } -> pub
| I_reexport _ -> true
(* 테스트는 표면이 아니다. 테스트를 고쳤다고 downstream이 재검사되면
안 된다 — 함수 본문과 같은 이유다 *)
| I_test _ -> false
| I_import _ -> false
let strip = function
| I_fn { pub; decl } -> I_fn { pub; decl = strip_body decl }
| it -> it
let item_name = function
| I_fn { decl; _ } -> decl.fn_name
| I_struct { name; _ } -> name
| I_enum { name; _ } -> name
| I_capability { name; _ } -> name
| I_const { name; _ } -> name
| I_reexport { name; _ } -> name
| I_test { name; _ } -> name
| I_import { alias; _ } -> alias
(* reexport는 이름이 아니라 해소된 정의 본문이 hash에 들어간다.
A의 enum에 variant가 추가되면 B의 소스가 그대로여도 B의 hash가 변하고,
C의 exhaustive match가 재검사된다. *)
let surface (m : modul) : item list =
let is_definition = function
| I_reexport _ | I_import _ -> false
| _ -> true
in
let find name =
List.find_opt (fun it -> is_definition it && item_name it = name) m.items
in
List.concat_map
(fun it ->
match it with
| I_reexport { name; _ } -> (
match find name with Some d -> [ strip d ] | None -> [])
| it when is_exported it && is_definition it -> [ strip it ]
| _ -> [])
m.items
(* 정규형: 항목을 이름순으로 정렬해 선언 순서가 해시에 새지 않게 한다.
소스에서 함수 둘의 위치를 바꾸는 것은 downstream에 아무 영향이 없다. *)
let render (items : item list) : string =
items |> List.map show_item |> List.sort compare |> String.concat "\n"
let of_module (m : modul) : t =
let items = surface m in
{ items; hash = Digest.to_hex (Digest.string (render items)) }
(* ------------------------------------------------------------------ *)
(* 소비 측 한정 *)
(* ------------------------------------------------------------------ *)
(* 가져온 모듈의 exported surface를 소비 측 이름 공간으로 옮긴다.
`import "shapes" as Shapes`라면 Shape는 "Shapes.Shape"가 된다.
왜 소비 시점인가 — 별칭은 가져오는 쪽의 선택이므로 정의한 모듈의
interface hash에 새어서는 안 된다. of_module은 한정하지 않은 표면을
해시하고, 한정은 여기서만 한다.
v0의 한계 두 가지, 의도적으로 남긴다:
- 가져온 모듈이 다시 다른 모듈의 타입을 참조하면(전이 참조) 불투명해진다.
- effect atom의 capability 이름은 한정하지 않는다. 즉 effect 이름은 v0에서
전역이다. 모듈별 identity는 v1 과제다. *)
let builtin_ty_names =
[ "Int"; "Bool"; "String"; "Unit"; "List"; "Option"; "Result"; "TaskScope" ]
let opaque pos = T_named { modl = None; name = "«외부»"; args = []; pos }
let rec q_ty alias defined gen (t : ty) : ty =
match t with
| T_named { modl = Some _; pos; _ } -> opaque pos
| T_named { modl = None; name; args; pos } ->
let args = List.map (q_targ alias defined gen) args in
if List.mem name gen || List.mem name builtin_ty_names then
T_named { modl = None; name; args; pos }
else if List.mem name defined then
T_named { modl = None; name = alias ^ "." ^ name; args; pos }
else opaque pos
| T_fn { affine; params; eff; ret; pos } ->
T_fn
{
affine;
params =
List.map
(fun (p : fn_param_ty) ->
{ p with pt_ty = q_ty alias defined gen p.pt_ty })
params;
eff;
ret = Option.map (q_ty alias defined gen) ret;
pos;
}
and q_targ alias defined gen = function
| TA_ty t -> TA_ty (q_ty alias defined gen t)
| TA_eff e -> TA_eff e
let q_fn alias defined (d : fn_decl) : fn_decl =
let gen = List.map (fun g -> g.gp_name) d.fn_gen in
{
d with
fn_params =
List.map
(fun p -> { p with p_ty = q_ty alias defined gen p.p_ty })
d.fn_params;
fn_ret = Option.map (q_ty alias defined gen) d.fn_ret;
fn_body = None;
}
let defined_names (items : item list) =
List.filter_map
(function
| I_struct { name; _ } | I_enum { name; _ } | I_capability { name; _ } ->
Some name
| _ -> None)
items
let qualify (alias : string) (items : item list) : item list =
let defined = defined_names items in
let p n = alias ^ "." ^ n in
List.map
(fun it ->
match it with
| I_fn { pub; decl } ->
I_fn
{
pub;
decl = { (q_fn alias defined decl) with fn_name = p decl.fn_name };
}
| I_struct { pub; copyable; name; gen; fields; pos } ->
let g = List.map (fun x -> x.gp_name) gen in
I_struct
{
pub;
copyable;
name = p name;
gen;
fields =
List.map
(fun f -> { f with f_ty = q_ty alias defined g f.f_ty })
fields;
pos;
}
| I_enum { pub; name; gen; variants; pos } ->
let g = List.map (fun x -> x.gp_name) gen in
I_enum
{
pub;
name = p name;
gen;
variants =
List.map
(fun v ->
{
v with
v_name = p v.v_name;
v_args = List.map (q_ty alias defined g) v.v_args;
})
variants;
pos;
}
| I_capability { pub; name; methods; pos } ->
I_capability
{
pub;
name = p name;
methods = List.map (q_fn alias defined) methods;
pos;
}
| I_const { pub; name; ty; value; pos } ->
I_const
{ pub; name = p name; ty = q_ty alias defined [] ty; value; pos }
| it -> it)
items
+553
View File
@@ -0,0 +1,553 @@
(* 트리 워킹 인터프리터.
여기 도달한 프로그램은 이미 타입, effect, capability, ownership 검사를
통과했다. 그러므로 이 파일은 검사하지 않는다 — 검사기가 이미 답한 질문을
실행 시점에 다시 묻는 것은 두 번째 진실을 만드는 일이다.
실행 시점 오류로 남는 것은 검사기가 원리적으로 못 잡는 것뿐이다:
0으로 나누기, 리스트 범위, 그리고 아직 없는 표준 라이브러리 이름.
권한의 유일한 출처는 런타임이다. 소스에는 capability를 만드는 문법이 없고,
main은 자기가 선언한 capability만 받는다. 선언하지 않은 권한은 프로그램
어디에도 존재하지 않는다 — 보안 정리 (i)의 실행 시점 대응물이다. *)
type value =
| VUnit
| VInt of int
| VBool of bool
| VStr of string
| VList of value list
| VStruct of string * (string * value ref) list
| VEnum of string * string * value list (* enum, variant, 인자 *)
| VClosure of { params : string list; body : Ir.t; env : env }
| VFn of Ir.fn
| VCtor of string * string * int (* 아직 인자를 안 받은 생성자 *)
| VBuiltin of string
| VNative of (value list -> value)
| VCap of string * (string * (value list -> value)) list
| VScope of string
and env = (string * value ref) list list
exception Return_exc of value
exception Fail of Token.pos * string
let fail pos msg = raise (Fail (pos, msg))
let rec show = function
| VUnit -> "unit"
| VInt n -> string_of_int n
| VBool b -> if b then "true" else "false"
| VStr s -> s
| VList xs -> "[" ^ String.concat ", " (List.map show xs) ^ "]"
| VStruct (n, fs) ->
n ^ "{"
^ String.concat ", " (List.map (fun (k, v) -> k ^ ": " ^ show !v) fs)
^ "}"
| VEnum (_, v, []) -> v
| VEnum (_, v, args) ->
v ^ "(" ^ String.concat ", " (List.map show args) ^ ")"
| VClosure _ | VFn _ | VBuiltin _ | VCtor _ | VNative _ -> "<fn>"
| VCap (n, _) -> "<capability " ^ n ^ ">"
| VScope n -> "<scope " ^ n ^ ">"
let rec eq a b =
match (a, b) with
| VInt x, VInt y -> x = y
| VBool x, VBool y -> x = y
| VStr x, VStr y -> x = y
| VUnit, VUnit -> true
| VList x, VList y -> List.length x = List.length y && List.for_all2 eq x y
| VEnum (_, v1, a1), VEnum (_, v2, a2) ->
v1 = v2 && List.length a1 = List.length a2 && List.for_all2 eq a1 a2
| _ -> false
(* ------------------------------------------------------------------ *)
(* 환경 *)
(* ------------------------------------------------------------------ *)
let lookup (env : env) n =
let rec go = function
| [] -> None
| s :: r -> (
match List.assoc_opt n s with Some v -> Some v | None -> go r)
in
go env
let bind (env : env) n v : env =
match env with s :: r -> ((n, ref v) :: s) :: r | [] -> [ [ (n, ref v) ] ]
(* ------------------------------------------------------------------ *)
(* 런타임이 제공하는 것 *)
(* ------------------------------------------------------------------ *)
(* 문자열 도우미. 언어에 인덱싱 연산자가 없으므로 이 일은 런타임 몫이다. *)
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
(* 프로그램 인자. 런타임이 들고 있다가 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 =
match name with
| "Console" ->
Some
(VCap
( "Console",
[
( "print",
fun args ->
List.iter (fun v -> Buffer.add_string out (show v)) args;
Buffer.add_char out '\n';
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" ->
(* 루트 스코프. 구조적 동시성의 뿌리도 런타임이 준다 — 프로그램이
스스로 만들 수 있으면 부모 없는 작업이 생긴다. *)
Some (VScope "root")
| _ -> 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.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 =
match (name, args) with
| "string.concat", [ VStr a; VStr b ] -> VStr (a ^ b)
| "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.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")
| "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.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)
(* ------------------------------------------------------------------ *)
(* 실행 *)
(* ------------------------------------------------------------------ *)
type st = { prog : Ir.program }
let rec eval st (env : env) (e : Ir.t) : value =
match e with
| Ir.I_unit -> VUnit
| Ir.I_lit (Ast.L_int n) -> VInt (int_of_string n)
| Ir.I_lit (Ast.L_str s) -> VStr s
| Ir.I_lit (Ast.L_bool b) -> VBool b
| Ir.I_ref (k, pos) -> eval_ref st env k pos
| Ir.I_list xs -> VList (List.map (eval st env) xs)
| Ir.I_make (n, fields) ->
VStruct (n, List.map (fun (k, e) -> (k, ref (eval st env e))) fields)
| Ir.I_closure c -> VClosure { params = c.c_params; body = c.c_body; env }
| Ir.I_if { cond; then_; else_ } -> (
match eval st env cond with
| VBool true -> eval st ([] :: env) then_
| _ -> eval st ([] :: env) else_)
| Ir.I_match { scrutinee; arms; pos } ->
let v = eval st env scrutinee in
let rec go = function
| [] -> fail pos "match에서 어떤 팔도 맞지 않았습니다"
| (p, body) :: rest -> (
match match_pat v p with
| None -> go rest
| Some binds ->
let env =
List.fold_left (fun e (n, v) -> bind e n v) ([] :: env) binds
in
eval st env body)
in
go arms
| Ir.I_scope { name; body; _ } ->
(* v0의 실행 의미: 자식 작업은 순차로 돈다. 블록을 나가는 것이 join이다.
구조가 먼저고 병렬성은 그 위의 최적화다 — 순서가 반대면 취소와
전파를 나중에 끼워 넣게 된다. *)
let env = bind ([] :: env) name (VScope name) in
eval st env body
| Ir.I_seq (stmts, tail) ->
let env = List.fold_left (fun env s -> exec st env s) ([] :: env) stmts in
eval st env tail
| Ir.I_call { callee; args; pos } ->
let f = eval st env callee in
let args = List.map (eval st env) args in
apply st pos f args
| Ir.I_field { obj; name; pos } -> (
match eval st env obj with
| VStruct (sn, fields) -> (
match List.assoc_opt name fields with
| Some r -> !r
| None -> fail pos (Printf.sprintf "%s에 %s 필드가 없습니다" sn name))
| VCap (cn, meths) -> (
(* capability 메서드는 값을 통해서만 나온다. 여기가 권한이 코드로
흐르는 유일한 통로다. *)
match List.assoc_opt name meths with
| Some f -> VNative f
| None ->
fail pos (Printf.sprintf "capability %s에 %s이(가) 없습니다" cn name))
| VScope _ when name = "spawn" ->
VNative
(fun args ->
match args with
| [ f ] -> apply st pos f []
| _ -> fail pos "spawn은 함수 하나를 받습니다")
| 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) -> (
match (op, eval st env e) with
| Ast.U_not, VBool b -> VBool (not b)
| Ast.U_neg, VInt n -> VInt (-n)
| _ -> fail pos "단항 연산자의 피연산자가 맞지 않습니다")
| Ir.I_binary (op, a, b, pos) -> eval_binary st env op a b pos
and eval_ref st env k pos =
match k with
| Ir.R_local n -> (
match lookup env n with
| Some r -> !r
| None -> fail pos (Printf.sprintf "%s이(가) 묶여 있지 않습니다" n))
| Ir.R_ctor (enum, name) -> (
match Hashtbl.find_opt st.prog.Ir.ctors name with
| Some (_, 0) -> VEnum (enum, name, [])
| Some (_, n) -> VCtor (enum, name, n)
| None -> VEnum (enum, name, []))
| Ir.R_global n -> (
match Hashtbl.find_opt st.prog.Ir.fns n with
| Some f -> VFn f
| None -> (
match Hashtbl.find_opt st.prog.Ir.consts n with
| Some e -> eval st [ [] ] e
| None -> fail pos (Printf.sprintf "%s을(를) 찾을 수 없습니다" n)))
| Ir.R_builtin n -> VBuiltin n
and apply st pos f args =
match f with
| VFn fn -> (
let env = [ List.map2 (fun p a -> (p, ref a)) fn.Ir.fn_params args ] in
try eval st env fn.Ir.fn_body with Return_exc v -> v)
| VClosure { params; body; env } -> (
let env = List.map2 (fun p a -> (p, ref a)) params args :: env in
try eval st env body with Return_exc v -> v)
| VCtor (enum, name, _) -> VEnum (enum, name, args)
(* 고차 builtin은 여기서 처리한다 — apply를 다시 부를 수 있어야 하므로 *)
| VBuiltin "list.each" -> (
match args with
| [ VList xs; f ] ->
List.iter (fun x -> ignore (apply st pos f [ x ])) xs;
VUnit
| _ -> fail pos "list.each는 리스트와 함수를 받습니다")
| VBuiltin "list.map" -> (
match args with
| [ VList xs; f ] -> VList (List.map (fun x -> apply st pos f [ x ]) xs)
| _ -> 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
| VNative f -> f args
| other -> fail pos (Printf.sprintf "%s은(는) 부를 수 없습니다" (show other))
and exec st env (s : Ir.stmt) : env =
match s with
| Ir.S_do e ->
ignore (eval st env e);
env
| Ir.S_return (e, _) -> raise (Return_exc (eval st env e))
| Ir.S_let (p, e, pos) -> (
let v = eval st env e in
match match_pat v p with
| None -> fail pos "let 패턴이 값과 맞지 않습니다"
| Some binds -> List.fold_left (fun e (n, v) -> bind e n v) env binds)
| Ir.S_assign { place; value; pos } -> (
let v = eval st env value in
match place with
| Ir.I_ref (Ir.R_local n, _) -> (
match lookup env n with
| Some r ->
r := v;
env
| None -> fail pos (Printf.sprintf "%s이(가) 묶여 있지 않습니다" n))
| Ir.I_field { obj; name; _ } -> (
match eval st env obj with
| VStruct (_, fields) -> (
match List.assoc_opt name fields with
| Some r ->
r := v;
env
| None -> fail pos (Printf.sprintf "%s 필드가 없습니다" name))
| _ -> fail pos "필드에 대입할 수 없습니다")
| _ -> fail pos "대입할 수 없는 자리입니다")
and match_pat v (p : Ir.pat) : (string * value) list option =
match (p, v) with
| Ir.IP_wild, _ -> Some []
| Ir.IP_bind n, _ -> Some [ (n, v) ]
| Ir.IP_lit (Ast.L_int n), VInt m ->
if int_of_string n = m then Some [] else None
| Ir.IP_lit (Ast.L_str s), VStr t -> if s = t then Some [] else None
| Ir.IP_lit (Ast.L_bool b), VBool c -> if b = c then Some [] else None
| Ir.IP_ctor (name, ps), VEnum (_, vn, args) ->
if name <> vn || List.length ps <> List.length args then None
else
List.fold_left2
(fun acc p a ->
match (acc, match_pat a p) with
| Some xs, Some ys -> Some (xs @ ys)
| _ -> None)
(Some []) ps args
| _ -> None
and eval_binary st env op a b pos =
match op with
(* 단축 평가. 오른쪽을 먼저 계산하면 && 의 의미가 달라진다 *)
| Ast.B_and -> (
match eval st env a with VBool false -> VBool false | _ -> eval st env b)
| Ast.B_or -> (
match eval st env a with VBool true -> VBool true | _ -> eval st env b)
| _ -> (
let x = eval st env a and y = eval st env b in
match (op, x, y) with
| Ast.B_eq, _, _ -> VBool (eq x y)
| Ast.B_ne, _, _ -> VBool (not (eq x y))
| Ast.B_lt, VInt m, VInt n -> VBool (m < n)
| Ast.B_le, VInt m, VInt n -> VBool (m <= n)
| Ast.B_gt, VInt m, VInt n -> VBool (m > n)
| Ast.B_ge, VInt m, VInt n -> VBool (m >= n)
| Ast.B_add, VInt m, VInt n -> VInt (m + n)
| Ast.B_sub, VInt m, VInt n -> VInt (m - n)
| Ast.B_mul, VInt m, VInt n -> VInt (m * n)
| Ast.B_div, VInt _, VInt 0 -> fail pos "0으로 나눌 수 없습니다"
| Ast.B_div, VInt m, VInt n -> VInt (m / n)
| Ast.B_rem, VInt _, VInt 0 -> fail pos "0으로 나눌 수 없습니다"
| Ast.B_rem, VInt m, VInt n -> VInt (m mod n)
| _ -> fail pos "연산자의 피연산자 타입이 맞지 않습니다")
(* ------------------------------------------------------------------ *)
(* 진입 *)
(* ------------------------------------------------------------------ *)
(* main이 선언한 capability만 런타임이 넘긴다. 선언하지 않은 권한은
프로그램 안에 존재하지 않는다. *)
(* 실패해도 그때까지 나온 출력을 함께 돌려준다.
print는 실제로 일어난 effect다. 일어난 일을 안 보여주면 "어디까지 갔나"를
알 수 없고, 그게 실패했을 때 가장 먼저 보고 싶은 것이다. *)
let run ?(args = []) (prog : Ir.program) (entry : string)
(main_params : (string * string) list) :
string * (Token.pos * string) option =
Buffer.clear out;
argv := args;
let st = { prog } in
match Hashtbl.find_opt prog.Ir.fns (entry ^ "#main") with
| None -> ("", Some (Token.{ line = 0; col = 0 }, "main 함수가 없습니다"))
| Some fn -> (
let args =
List.map
(fun (_, tyname) ->
match root_capability tyname with
| Some v -> Ok v
| None ->
Error (Printf.sprintf "런타임이 %s capability를 제공하지 않습니다" tyname))
main_params
in
match List.find_opt Result.is_error args with
| Some (Error m) -> ("", Some (Token.{ line = 0; col = 0 }, m))
| _ -> (
let args = List.map Result.get_ok args in
try
ignore (apply st Token.{ line = 0; col = 0 } (VFn fn) args);
(Buffer.contents out, None)
with
| Fail (pos, msg) -> (Buffer.contents out, Some (pos, msg))
| 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 })
+394
View File
@@ -0,0 +1,394 @@
(* 얇은 typed IR.
왜 AST를 직접 해석하지 않는가 — 표면 문법이 실행 의미에 닿지 않는다는 것을
구조로 강제하기 위해서다. v1이 백엔드를 무엇으로 바꾸든 소비하는 것은 이
IR이고, 문법을 고쳐도 여기가 그대로면 실행 의미는 그대로다.
얇다는 것의 뜻: 새 개념을 만들지 않는다. 검사 단계가 이미 답한 질문을
다시 묻지 않는다 — 여기 도달한 프로그램은 타입, effect, capability,
ownership 검사를 모두 통과했다. 그래서 IR에는 타입 검사가 없다.
낮추기에서 사라지는 것:
- E_try: Result에 대한 match로 펼친다. `?`는 설탕이다
- E_inst: 타입 인자는 실행에 영향이 없다 (단형화 없음, 값 표현이 같다)
- 한정 이름: "Alias.f" 하나의 이름으로 평탄화된다 (검사 단계와 같은 규칙) *)
type pos = Token.pos
(* 이름은 낮추기 시점에 분류된다. 실행 중에 "이게 지역인가 전역인가"를
다시 묻지 않는다. *)
(* 전역 이름은 정규화된다: "<모듈 경로>#<이름>". 별칭은 가져오는 쪽의 선택이라
실행 의미에 들어와서는 안 된다 — 같은 함수가 부르는 자리마다 다른 이름이 되면
IR은 더 이상 v1의 번역 대상이 아니다. *)
type ref_kind =
| R_local of string
| R_global of string (* "<경로>#<이름>" *)
| R_ctor of string * string (* enum 이름, 정규화된 variant 이름 *)
| R_builtin of string (* 본문 없는 선언 = 런타임이 구현한다 *)
type pat =
| IP_wild
| IP_lit of Ast.lit
| IP_bind of string
| IP_ctor of string * pat list (* variant 이름 *)
type t =
| I_unit
| I_lit of Ast.lit
| I_ref of ref_kind * pos
| I_list of t list
| I_make of string * (string * t) list (* struct 생성 *)
| I_closure of closure
| I_if of { cond : t; then_ : t; else_ : t }
| I_match of { scrutinee : t; arms : (pat * t) list; pos : pos }
| I_scope of { name : string; body : t; pos : pos }
| I_seq of stmt list * t (* 블록: 문 나열 + 꼬리 값 *)
| I_call of { callee : t; args : t list; pos : pos }
| I_field of { obj : t; name : string; pos : pos }
| I_crash of t * pos
| I_unary of Ast.unop * t * pos
| I_binary of Ast.binop * t * t * pos
and stmt =
| S_let of pat * t * pos
| S_assign of { place : t; value : t; pos : pos }
| S_return of t * pos
| S_do of t
and closure = { c_params : string list; c_body : t; c_pos : pos }
type fn = { fn_name : string; fn_params : string list; fn_body : t }
type program = {
fns : (string, fn) Hashtbl.t;
(* 테스트. (모듈 경로, 이름, 위치, 본문) *)
mutable tests : (string * string * pos * t) list;
(* variant 이름 -> (enum 이름, 인자 개수) *)
ctors : (string, string * int) Hashtbl.t;
caps : (string, string list) Hashtbl.t; (* capability -> 메서드 이름 *)
consts : (string, t) Hashtbl.t;
}
(* ------------------------------------------------------------------ *)
(* 낮추기 *)
(* ------------------------------------------------------------------ *)
let qual modl name = match modl with Some a -> a ^ "." ^ name | None -> name
(* 지역 이름 스택. 검사 단계가 아니라 분류만 한다 — 여기서 못 찾은 이름은
전역이거나 생성자이거나 런타임 제공이다. *)
type modinfo = {
m_path : string; (* 정규 경로 *)
m_ast : Ast.modul;
m_deps : (string * string) list; (* 별칭 -> 정규 경로 *)
}
(* 본문 없는 선언은 런타임이 구현한다. 그 이름은 모듈 파일 이름에서 온다:
std/list.cool의 each는 "list.each"다. 가져오는 쪽의 별칭과 무관하다. *)
let builtin_name path name =
Filename.remove_extension (Filename.basename path) ^ "." ^ name
type lctx = {
prog : program;
mods : (string, modinfo) Hashtbl.t;
cur : modinfo;
mutable locals : string list list;
}
let lpush c = c.locals <- [] :: c.locals
let lpop c = match c.locals with _ :: r -> c.locals <- r | [] -> ()
let lbind c n =
match c.locals with
| s :: r -> c.locals <- (n :: s) :: r
| [] -> c.locals <- [ [ n ] ]
let is_local c n = List.exists (fun s -> List.mem n s) c.locals
let builtin_ctors = [ "Ok"; "Err"; "Some"; "None" ]
(* 한 모듈 안에서 이름 하나를 분류한다. 지역이 아니면 그 모듈의 정의를 본다. *)
let in_module c (mi : modinfo) name =
let key = mi.m_path ^ "#" ^ name in
if Hashtbl.mem c.prog.ctors key then
match Hashtbl.find_opt c.prog.ctors key with
| Some (enum, _) -> Some (R_ctor (enum, key))
| None -> None
else if Hashtbl.mem c.prog.consts key then Some (R_global key)
else
let decl =
List.find_map
(function
| Ast.I_fn { decl; _ } when decl.fn_name = name -> Some decl
| _ -> None)
mi.m_ast.items
in
match decl with
| Some d when d.fn_body <> None -> Some (R_global key)
| Some _ -> Some (R_builtin (builtin_name mi.m_path name))
| None -> None
let classify c name =
if is_local c name then R_local name
else if List.mem name builtin_ctors then
R_ctor ((if name = "Ok" || name = "Err" then "Result" else "Option"), name)
else
match in_module c c.cur name with
| Some k -> k
| None -> (
(* 한정 이름 "A.f": A가 이 모듈의 import면 그 모듈에서 찾는다. *)
match String.index_opt name '.' with
| None -> R_builtin name
| Some i -> (
let a = String.sub name 0 i in
let f = String.sub name (i + 1) (String.length name - i - 1) in
match List.assoc_opt a c.cur.m_deps with
| None -> R_builtin name
| Some path -> (
match Hashtbl.find_opt c.mods path with
| None -> R_builtin name
| Some mi -> (
match in_module c mi f with
| Some k -> k
| None -> R_builtin (builtin_name path f)))))
(* 패턴의 생성자도 정규 이름으로 접는다. 값 쪽과 같은 키를 써야 match가
성립한다 — 두 곳이 다른 규칙을 쓰면 조용히 안 맞는다. *)
let ctor_key c modl name : string option =
if List.mem name builtin_ctors then Some name
else
let path =
match modl with
| None -> Some c.cur.m_path
| Some a -> List.assoc_opt a c.cur.m_deps
in
match path with
| None -> None
| Some p ->
let k = p ^ "#" ^ name in
if Hashtbl.mem c.prog.ctors k then Some k else None
let rec lower_pat c (p : Ast.pattern) : pat =
match p with
| Ast.P_wild _ -> IP_wild
| Ast.P_lit (l, _) -> IP_lit l
| Ast.P_bind (n, _) -> (
(* 인자 없는 생성자는 이름만 쓴다. 바인딩과 구별은 여기서 끝난다. *)
match ctor_key c None n with
| Some k -> IP_ctor (k, [])
| None ->
lbind c n;
IP_bind n)
| Ast.P_ctor { modl; name; args; _ } ->
let k =
match ctor_key c modl name with Some k -> k | None -> qual modl name
in
IP_ctor (k, List.map (lower_pat c) args)
let rec lower c (e : Ast.expr) : t =
match e with
| Ast.E_crash { msg; pos } -> I_crash (lower c msg, pos)
| Ast.E_lit (l, _) -> I_lit l
| Ast.E_ident (n, pos) -> I_ref (classify c n, pos)
| Ast.E_list (xs, _) -> I_list (List.map (lower c) xs)
| Ast.E_struct { name; fields; _ } ->
I_make (name, List.map (fun (n, e) -> (n, lower c e)) fields)
| Ast.E_closure cl ->
lpush c;
List.iter (fun (p : Ast.cl_param) -> lbind c p.cp_name) cl.cl_params;
let body = lower_block c cl.cl_body in
lpop c;
I_closure
{
c_params = List.map (fun (p : Ast.cl_param) -> p.cp_name) cl.cl_params;
c_body = body;
c_pos = cl.cl_pos;
}
| Ast.E_if { cond; then_; else_; _ } ->
let cond = lower c cond in
lpush c;
let t = lower_block c then_ in
lpop c;
let e =
match else_ with
| None -> I_unit
| Some e ->
lpush c;
let v = lower c e in
lpop c;
v
in
I_if { cond; then_ = t; else_ = e }
| Ast.E_match { scrutinee; arms; pos } ->
let s = lower c scrutinee in
let arms =
List.map
(fun (a : Ast.arm) ->
lpush c;
let p = lower_pat c a.arm_pat in
let b = lower c a.arm_body in
lpop c;
(p, b))
arms
in
I_match { scrutinee = s; arms; pos }
| Ast.E_scope { name; body; pos; _ } ->
lpush c;
lbind c name;
let b = lower_block c body in
lpop c;
I_scope { name; body = b; pos }
| Ast.E_block b ->
lpush c;
let v = lower_block c b in
lpop c;
v
| Ast.E_call { callee; args; pos } ->
I_call { callee = lower c callee; args = List.map (lower c) args; pos }
| Ast.E_field { obj; name; pos } -> (
(* Alias.f / String.concat 처럼 이름공간 접근은 하나의 이름으로 접는다.
값의 필드 접근과 구별되는 지점은 obj가 지역 이름이 아닌 것뿐이다. *)
match obj with
| Ast.E_ident (o, _) when not (is_local c o) ->
I_ref (classify c (o ^ "." ^ name), pos)
| _ -> I_field { obj = lower c obj; name; pos })
| Ast.E_inst { callee; _ } -> lower c callee
| Ast.E_try { inner; pos } ->
(* `?`는 설탕이다: Ok(v) => v, Err(e) => return Err(e) *)
I_match
{
scrutinee = lower c inner;
arms =
[
(IP_ctor ("Ok", [ IP_bind "?v" ]), I_ref (R_local "?v", pos));
( IP_ctor ("Err", [ IP_bind "?e" ]),
I_seq
( [
S_return
( I_call
{
callee = I_ref (R_ctor ("Result", "Err"), pos);
args = [ I_ref (R_local "?e", pos) ];
pos;
},
pos );
],
I_unit ) );
];
pos;
}
| Ast.E_unary { op; operand; pos } -> I_unary (op, lower c operand, pos)
| Ast.E_binary { op; lhs; rhs; pos } ->
I_binary (op, lower c lhs, lower c rhs, pos)
and lower_block c (b : Ast.block) : t =
let rec go = function
| [] -> I_unit
| [ Ast.S_expr e ] -> lower c e
| s :: rest -> (
let s = lower_stmt c s in
let tail = go rest in
match tail with
| I_seq (ss, t) -> I_seq (s :: ss, t)
| t -> I_seq ([ s ], t))
in
go b.stmts
and lower_stmt c (s : Ast.stmt) : stmt =
match s with
| Ast.S_let { pat; value; pos; _ } ->
let v = lower c value in
(* 값을 먼저 낮춘다 — 바인딩은 그 뒤에야 보인다 *)
S_let (lower_pat c pat, v, pos)
| Ast.S_return { value; pos } ->
S_return ((match value with Some e -> lower c e | None -> I_unit), pos)
| Ast.S_assign { place; value; pos } ->
S_assign { place = lower c place; value = lower c value; pos }
| Ast.S_expr e -> S_do (lower c e)
(* 모듈 그래프 전체를 하나의 IR 프로그램으로 낮춘다.
모듈 하나씩 낮추고 싶은 유혹이 있지만, 그러면 별칭이 실행 의미에 남는다.
같은 함수가 A에서는 Shapes.double, B에서는 Geo.double이 되어 IR이 더 이상
프로그램의 정규형이 아니게 된다. 정규 경로로 한 번 접는다. *)
let of_program (mods : modinfo list) : program =
let prog =
{
fns = Hashtbl.create 64;
tests = [];
ctors = Hashtbl.create 64;
caps = Hashtbl.create 16;
consts = Hashtbl.create 16;
}
in
let index = Hashtbl.create 16 in
List.iter (fun mi -> Hashtbl.replace index mi.m_path mi) mods;
(* 1차: 이름부터. 낮추기가 이름을 분류하려면 그래프 전체가 먼저 보여야 한다. *)
List.iter
(fun mi ->
let key n = mi.m_path ^ "#" ^ n in
List.iter
(fun (it : Ast.item) ->
match it with
| Ast.I_enum { name; variants; _ } ->
List.iter
(fun (v : Ast.variant) ->
Hashtbl.replace prog.ctors (key v.v_name)
(name, List.length v.v_args))
variants
| Ast.I_capability { name; methods; _ } ->
Hashtbl.replace prog.caps name
(List.map (fun (d : Ast.fn_decl) -> d.fn_name) methods)
| Ast.I_const { name; _ } ->
Hashtbl.replace prog.consts (key name) I_unit
| _ -> ())
mi.m_ast.items)
mods;
List.iter
(fun (v, e, n) -> Hashtbl.replace prog.ctors v (e, n))
[
("Ok", "Result", 1);
("Err", "Result", 1);
("Some", "Option", 1);
("None", "Option", 0);
];
(* 2차: 본문 *)
List.iter
(fun mi ->
let c = { prog; mods = index; cur = mi; locals = [] } in
let key n = mi.m_path ^ "#" ^ n in
List.iter
(fun (it : Ast.item) ->
match it with
| Ast.I_fn { decl; _ } -> (
match decl.fn_body with
| None -> ()
| Some body ->
c.locals <- [];
lpush c;
List.iter
(fun (p : Ast.param) -> lbind c p.p_name)
decl.fn_params;
let b = lower_block c body in
lpop c;
Hashtbl.replace prog.fns (key decl.fn_name)
{
fn_name = key decl.fn_name;
fn_params =
List.map
(fun (p : Ast.param) -> p.p_name)
decl.fn_params;
fn_body = b;
})
| Ast.I_const { name; value; _ } ->
c.locals <- [];
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)
mods;
prog
+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;
]
+549
View File
@@ -0,0 +1,549 @@
(* move / affinity 검사.
보안 정리 (ii) — safe code에서 capability는 복제·위조되지 않는다 — 를 코드로
닫는 단계다. 검사는 전부 함수 로컬 데이터플로우다. 전역 분석이 없다.
affinity의 뿌리는 capability다. capability를 필드로 가진 타입은 전이적으로
affine이고(철학 3), 이 전이가 없으면 wrapper 하나를 복사해 capability가
사실상 복제된다.
외부 타입은 affine임을 증명할 수 없으므로 copyable로 본다 — 모르는 것을
위반이라고 말하지 않는다. 그래서 이 검사를 시험하려면 자원 타입을 모듈
안에서 정의해야 한다 (samples/05). *)
open Ast
type error = { pos : Token.pos; msg : string }
(* 값이 무엇인가: 소유한 affine 값인가, 빌린 값인가. 둘은 직교한다. *)
type vinfo = { v_affine : bool; v_use : bool }
let v_copy = { v_affine = false; v_use = false }
type binding = {
b_id : int;
b_name : string;
b_affine : bool;
b_use : bool;
b_mut : bool;
b_depth : int;
(* 선언된 타입. 필드가 affine인지 알려면 필요하다 — 모르면 필드에서
꺼내는 것을 막을 수 없고, 그러면 capability가 struct를 통해 복제된다. *)
b_ty : ty option;
}
type fninfo = { f_params : param list; f_ret : Ast.ty option }
type state = {
aff : (string, bool) Hashtbl.t;
(* struct 이름 -> 필드 이름과 타입 *)
fields : (string, (string * ty) list) Hashtbl.t;
fns : (string, fninfo) Hashtbl.t;
meths : (string, (string * fninfo) list) Hashtbl.t;
mutable scopes : binding list list;
moved : (int, Token.pos) Hashtbl.t;
mutable next_id : int;
mutable depth : int;
(* 클로저 프레임: (프레임 깊이, 잡아온 바깥 바인딩). 중첩 클로저를 위해 스택 *)
mutable frames : (int * binding list ref) list;
(* 클로저 인자를 걸을 때 기대 파라미터 타입. move 검사는 타입 검사와 별도
순회라 타입을 모르는데, 클로저 파라미터의 affinity를 알아야 빌림 여부를
판정할 수 있다. 호출 대상의 시그니처에서 읽어와 여기 잠깐 둔다. *)
mutable cl_expect : fn_param_ty list option;
mutable errors : error list;
}
let err st pos msg = st.errors <- { pos; msg } :: st.errors
(* ------------------------------------------------------------------ *)
(* affinity 유도 *)
(* ------------------------------------------------------------------ *)
let builtin_containers = [ "List"; "Option"; "Result" ]
let rec ty_affine st (t : Ast.ty) =
match t with
| T_fn { affine; _ } -> affine
| T_named { modl; name; args; _ } ->
let name = match modl with Some a -> a ^ "." ^ name | None -> name in
let self =
match Hashtbl.find_opt st.aff name with Some b -> b | None -> false
in
let arg_affine =
List.exists
(function TA_ty t -> ty_affine st t | TA_eff _ -> false)
args
in
self || (List.mem name builtin_containers && arg_affine) || arg_affine
(* capability가 뿌리다. struct/enum은 필드에서 전이된다. 상호 재귀 타입을 위해
변화가 없을 때까지 돈다. *)
let derive_affinity st (items : item list) =
List.iter
(fun it ->
match it with
| I_capability { name; _ } -> Hashtbl.replace st.aff name true
| 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
| _ -> ())
items;
let changed = ref true in
while !changed do
changed := false;
List.iter
(fun it ->
let update name affine =
if affine && Hashtbl.find_opt st.aff name <> Some true then (
Hashtbl.replace st.aff name true;
changed := true)
in
match it with
| I_struct { name; fields; _ } ->
update name (List.exists (fun f -> ty_affine st f.f_ty) fields)
| I_enum { name; variants; _ } ->
update name
(List.exists
(fun v -> List.exists (ty_affine st) v.v_args)
variants)
| _ -> ())
items
done;
(* copyable 선언과 affine 필드는 공존할 수 없다 *)
List.iter
(fun it ->
match it with
| I_struct { copyable = true; name; fields; pos; _ } ->
List.iter
(fun f ->
if ty_affine st f.f_ty then
err st f.f_pos
(Printf.sprintf "%s은(는) copyable로 선언되었지만 %s 필드가 affine입니다"
name f.f_name))
fields;
ignore pos
| _ -> ())
items
(* ------------------------------------------------------------------ *)
(* 스코프 *)
(* ------------------------------------------------------------------ *)
let push st = st.scopes <- [] :: st.scopes
let pop st = match st.scopes with _ :: r -> st.scopes <- r | [] -> ()
let add st name ~affine ~use ~mut_ ?ty () =
st.next_id <- st.next_id + 1;
let b =
{
b_id = st.next_id;
b_name = name;
b_affine = affine;
b_use = use;
b_mut = mut_;
b_depth = st.depth;
b_ty = ty;
}
in
(match st.scopes with
| s :: r -> st.scopes <- (b :: s) :: r
| [] -> st.scopes <- [ [ b ] ]);
b
let find st name =
let rec go = function
| [] -> None
| s :: r -> (
match List.find_opt (fun b -> b.b_name = name) s with
| Some b -> Some b
| None -> go r)
in
go st.scopes
(* 클로저 안에서 바깥 바인딩을 건드리면 capture다. 프레임마다 기록한다. *)
let note_capture st b =
List.iter
(fun (fdepth, acc) ->
if b.b_depth < fdepth && not (List.exists (fun x -> x.b_id = b.b_id) !acc)
then acc := b :: !acc)
st.frames
(* ------------------------------------------------------------------ *)
(* 분기 병합 — 보수적 합집합 *)
(* ------------------------------------------------------------------ *)
let snapshot st = Hashtbl.copy st.moved
let restore st snap =
Hashtbl.reset st.moved;
Hashtbl.iter (fun k v -> Hashtbl.replace st.moved k v) snap
let merge st snaps =
(* 한 분기에서라도 moved면 병합 지점 이후 moved *)
Hashtbl.reset st.moved;
List.iter
(fun snap ->
Hashtbl.iter
(fun k v ->
if not (Hashtbl.mem st.moved k) then Hashtbl.replace st.moved k v)
snap)
snaps
(* ------------------------------------------------------------------ *)
(**)
(* ------------------------------------------------------------------ *)
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
let rec walk st (ctx : ctx) (e : expr) : vinfo =
match e with
| E_lit _ -> v_copy
(* crash는 돌아오지 않지만 메시지 식은 계산된다. 메시지는 빌려 쓴다 *)
| E_crash { msg; _ } ->
ignore (walk st Borrow msg);
v_copy
| E_ident (n, pos) -> (
match find st n with
| None -> v_copy
| Some b ->
note_capture st b;
(match Hashtbl.find_opt st.moved b.b_id with
| Some mp ->
err st pos
(Printf.sprintf "%s은(는) 이미 move되었습니다 (%d:%d에서 소비)" n
mp.Token.line mp.Token.col)
| None -> ());
(match ctx with
| Borrow -> ()
| Move where ->
if b.b_use then
err st pos
(Printf.sprintf "%s은(는) 빌린 값이라 %s 없습니다 (own으로 받아야 합니다)" n
where)
else if b.b_affine then Hashtbl.replace st.moved b.b_id pos);
{ v_affine = b.b_affine; v_use = b.b_use })
| E_list (xs, _) ->
let infos = List.map (walk st (Move "컨테이너에 넣을 수")) xs in
{ v_affine = List.exists (fun i -> i.v_affine) infos; v_use = false }
| E_struct { name; fields; _ } ->
List.iter (fun (_, e) -> ignore (walk st (Move "struct에 저장할 수") e)) fields;
{
v_affine =
(match Hashtbl.find_opt st.aff name with
| Some b -> b
| None -> false);
v_use = false;
}
| E_closure c -> (
let r = walk_closure st c in
(* use의 전염: 빌린 값을 capture한 클로저는 그 자체가 빌린 값이라
소유를 가져가는 자리로 갈 수 없다. 별도의 nonescaping 개념 없이
use 규칙 하나로 닫힌다. *)
match ctx with
| Move where when r.v_use ->
err st c.cl_pos
(Printf.sprintf "빌린 값을 capture한 클로저는 %s 없습니다 (use 값은 탈출하지 못합니다)"
where);
r
| _ -> r)
| E_if { cond; then_; else_; _ } ->
ignore (walk st Borrow cond);
let before = snapshot st in
let t1 = walk_block st ctx then_ in
let s1 = snapshot st in
restore st before;
let t2 = match else_ with None -> v_copy | Some e -> walk st ctx e in
let s2 = snapshot st in
merge st [ s1; s2 ];
{ v_affine = t1.v_affine || t2.v_affine; v_use = t1.v_use || t2.v_use }
| E_match { scrutinee; arms; _ } ->
let sinfo = walk st Borrow scrutinee in
let before = snapshot st in
let results =
List.map
(fun a ->
restore st before;
push st;
bind_pattern st sinfo a.arm_pat;
let r = walk st ctx a.arm_body in
pop st;
(r, snapshot st))
arms
in
if results <> [] then merge st (List.map snd results);
{
v_affine = List.exists (fun (r, _) -> r.v_affine) results;
v_use = List.exists (fun (r, _) -> r.v_use) results;
}
| E_scope { name; parent; body; _ } ->
ignore (walk st Borrow (E_ident (parent, Token.{ line = 0; col = 0 })));
push st;
(* 자식 TaskScope는 second-class다 — 블록 밖으로 나갈 수 없다 *)
ignore (add st name ~affine:true ~use:true ~mut_:false ());
let r = walk_block st ctx body in
pop st;
r
| E_block b ->
push st;
let r = walk_block st ctx b in
pop st;
r
| E_call { callee; args; pos } -> walk_call st callee args pos
| E_field { obj; name; pos } ->
(* v0에 부분 move는 없다. 필드 접근은 빌림이고 결과도 빌린 값이다.
그러므로 Move 자리에 놓으면 오류다 — 이 검사가 없으면 capability를
struct에 넣고 필드를 두 번 읽어 복제할 수 있다 (보안 정리 ii). *)
let o = walk st Borrow obj in
let fa =
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_try { inner; _ } -> walk st ctx inner
| E_unary { operand; _ } -> walk st Borrow operand
| E_binary { lhs; rhs; _ } ->
ignore (walk st Borrow lhs);
ignore (walk st Borrow rhs);
v_copy
and bind_pattern st (info : vinfo) p =
match p with
| P_wild _ | P_lit _ -> ()
| P_bind (n, _) ->
ignore (add st n ~affine:info.v_affine ~use:info.v_use ~mut_:false ())
| P_ctor { args; _ } -> List.iter (bind_pattern st info) args
and walk_block st ctx (b : block) : vinfo =
let rec go = function
| [] -> v_copy
| [ S_expr e ] -> walk st ctx e (* 꼬리 식은 블록의 값이다 *)
| s :: rest ->
walk_stmt st s;
go rest
in
go b.stmts
and walk_stmt st = function
| S_let { mut_; pat; value; _ } ->
let info = walk st (Move "다른 이름에 묶을 수") value in
let rec bind p =
match p with
| P_bind (n, _) ->
ignore (add st n ~affine:info.v_affine ~use:info.v_use ~mut_ ())
| P_ctor { args; _ } -> List.iter bind args
| _ -> ()
in
bind pat
| S_return { value; _ } -> (
match value with
| None -> ()
| Some e -> ignore (walk st (Move "반환할 수") e))
| S_assign { place; value; _ } ->
ignore (walk st (Move "대입할 수") value);
ignore (walk st Borrow place)
| S_expr e -> ignore (walk st Borrow e)
(* 클로저: 무엇을 잡아왔는지가 클로저 자신의 성질을 정한다 (전이 규칙) *)
and walk_closure st (c : closure) : vinfo =
st.depth <- st.depth + 1;
let acc = ref [] in
st.frames <- (st.depth, acc) :: st.frames;
push st;
let expect = st.cl_expect in
st.cl_expect <- None;
List.iteri
(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;
ignore (walk_block st (Move "반환할 수") c.cl_body);
pop st;
st.frames <- List.tl st.frames;
st.depth <- st.depth - 1;
let captured = !acc in
let affine = ref false and use_ = ref false in
List.iter
(fun b ->
if b.b_mut then
err st c.cl_pos
(Printf.sprintf
"클로저는 mut 바인딩 %s을(를) capture할 수 없습니다 (v0에 참조가 없으므로 별칭도 복사도 만들지 \
않는다)"
b.b_name);
if b.b_use then use_ := true;
if b.b_affine && not b.b_use then (
(* by-move capture: 바깥에서는 여기서 소비된다 *)
affine := true;
if not (Hashtbl.mem st.moved b.b_id) then
Hashtbl.replace st.moved b.b_id c.cl_pos))
captured;
{ v_affine = !affine; v_use = !use_ }
and walk_call st callee args pos =
let info = callee_info st callee in
ignore (walk st Borrow callee);
let params = match info with Some f -> f.f_params | None -> [] in
List.iteri
(fun i a ->
let p = List.nth_opt params i 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 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
(* callable affinity: affine 클로저를 fn 자리에 넘길 수 없다 *)
match pty with
| Some (T_fn { affine = false; _ }) when got.v_affine && not got.v_use ->
err st pos
"affine 값을 capture한 클로저는 fn 자리에 넘길 수 없습니다 (affine fn이어야 합니다)"
| _ -> ())
args;
match info with
| Some { f_ret = Some t; _ } -> { v_affine = ty_affine st t; v_use = false }
| _ -> v_copy
and callee_info st callee =
match callee with
| 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 ->
Hashtbl.find_opt st.fns n
| E_field { obj = E_ident (o, _); name; _ } -> (
match find st o with
| Some _ -> (
(* 값의 메서드: 타입을 모르면 넘어간다. capability 메서드는 아래에서 *)
match
Hashtbl.fold
(fun _ methods acc ->
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
(* ------------------------------------------------------------------ *)
(* 선언 *)
(* ------------------------------------------------------------------ *)
let check_fn st (d : fn_decl) =
match d.fn_body with
| None -> ()
| Some body ->
st.scopes <- [];
Hashtbl.reset st.moved;
st.depth <- 0;
st.frames <- [];
push st;
List.iter
(fun p ->
let affine = ty_affine st p.p_ty in
(* 무표기 = 빌림. own만이 소유 이전이다. *)
let use_ = affine && not p.p_own in
ignore (add st p.p_name ~affine ~use:use_ ~mut_:p.p_mut ~ty:p.p_ty ()))
d.fn_params;
let r = walk_block st (Move "반환할 수") body in
(match d.fn_ret with
| Some (T_fn { affine = false; _ }) when r.v_affine && not r.v_use ->
err st d.fn_pos
(Printf.sprintf
"%s이(가) affine 값을 capture한 클로저를 fn 타입으로 반환합니다 (affine fn이어야 합니다)"
d.fn_name)
| _ -> ());
pop st
let check ?(imports : item list = []) (m : modul) : error list =
let st =
{
aff = Hashtbl.create 16;
fields = Hashtbl.create 16;
fns = Hashtbl.create 16;
meths = Hashtbl.create 16;
scopes = [];
moved = Hashtbl.create 16;
next_id = 0;
depth = 0;
frames = [];
cl_expect = None;
errors = [];
}
in
derive_affinity st (imports @ m.items);
List.iter
(fun it ->
match it with
| I_fn { decl; _ } ->
Hashtbl.replace st.fns decl.fn_name
{ f_params = decl.fn_params; f_ret = decl.fn_ret }
| I_capability { name; methods; _ } ->
Hashtbl.replace st.meths name
(List.map
(fun d ->
(d.fn_name, { f_params = d.fn_params; f_ret = d.fn_ret }))
methods)
| _ -> ())
(imports @ m.items);
List.iter
(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;
List.sort
(fun a b ->
compare
(a.pos.Token.line, a.pos.Token.col)
(b.pos.Token.line, b.pos.Token.col))
(List.rev st.errors)
+112 -24
View File
@@ -129,8 +129,14 @@ let rec parse_ty st =
| Token.Ident n ->
let p = pos st in
adv st;
let modl, n =
if kind st = Token.Dot then (
adv st;
(Some n, ident st "타입 이름"))
else (None, n)
in
let args = if kind st = Token.LBracket then parse_targs st else [] in
T_named { name = n; args; pos = p }
T_named { modl; name = n; args; pos = p }
| _ -> err_expect st "타입"
and parse_fn_ty st affine p =
@@ -139,7 +145,8 @@ and parse_fn_ty st affine p =
if kind st = Token.RParen then []
else
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 kind st = Token.RParen then List.rev (t :: acc) else loop (t :: acc)
else List.rev (t :: acc)
@@ -190,8 +197,14 @@ let rec parse_pattern st =
| Token.Kw_false ->
adv st;
P_lit (L_bool false, p)
| Token.Ident n ->
| Token.Ident n -> (
adv st;
let modl, n =
if kind st = Token.Dot then (
adv st;
(Some n, ident st "생성자 이름"))
else (None, n)
in
if kind st = Token.LParen then (
adv st;
let rec loop acc =
@@ -203,8 +216,11 @@ let rec parse_pattern st =
in
let args = if kind st = Token.RParen then [] else loop [] in
expect_close st Token.RParen ")";
P_ctor { name = n; args; pos = p })
else P_bind (n, p)
P_ctor { modl; name = n; args; pos = p })
else
match modl with
| Some _ -> P_ctor { modl; name = n; args = []; pos = p }
| None -> P_bind (n, p))
| _ -> err_expect st "패턴"
(* ------------------------------------------------------------------ *)
@@ -383,6 +399,12 @@ and parse_primary st =
| Token.Kw_fn -> parse_closure st
| Token.Kw_if -> parse_if 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 ->
adv st;
let name = ident st "새 scope 이름" in
@@ -428,12 +450,14 @@ and parse_closure st =
if kind st = Token.RParen then []
else
let rec loop acc =
let own = accept st Token.Kw_own in
let n = ident st "파라미터 이름" 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 kind st = Token.RParen then List.rev ((n, t) :: acc)
else loop ((n, t) :: acc)
else List.rev ((n, t) :: acc)
if kind st = Token.RParen then List.rev (cp :: acc)
else loop (cp :: acc)
else List.rev (cp :: acc)
in
loop []
in
@@ -549,16 +573,11 @@ and parse_stmt st =
S_return { value; pos = p }
| _ ->
let e = parse_expr st in
if accept st Token.Eq then begin
let rec is_place = function
| E_ident _ -> true
| E_field { obj; _ } -> is_place obj
| _ -> false
in
if not (is_place e) then err st "대입 왼쪽에는 변수나 필드만 올 수 있습니다";
let value = parse_expr st in
S_assign { place = e; value; pos = p }
end
if accept st Token.Eq then
(* 대입 왼쪽에 무엇이 올 수 있는지는 구문이 아니라 이름 해소가
판정한다. 구문으로 가르면 ident 하나로 대입과 식이 갈리지 않아
문법이 LL(1)이 아니게 된다 (grammar.ebnf의 expr_stmt). *)
S_assign { place = e; value = parse_expr st; pos = p }
else S_expr e
(* ------------------------------------------------------------------ *)
@@ -749,6 +768,16 @@ let parse_item st =
adv st;
let name = ident st "재수출할 이름" in
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
match kind st with
@@ -769,20 +798,79 @@ let parse_item st =
I_const { pub; name; ty; value; pos = p }
| _ -> err_expect st "선언 (fn, struct, enum, capability, const)")
(* ------------------------------------------------------------------ *)
(* 오류 복구 *)
(* *)
(* 항목 단위로만 회복한다. 오류가 난 선언은 통째로 버리고 다음 선언에서 *)
(* 다시 시작한다 — 문 단위로 더 잘게 회복하려 하면 파서가 추측을 하게 되고, *)
(* 틀린 추측은 없는 오류를 지어낸다. 한 항목에 오류 하나가 상한이라는 것은 *)
(* 정직한 한계이지 숨길 것이 아니다. *)
(* *)
(* 동기화 지점: 중괄호 깊이 0이고, 줄 첫머리이며, 선언을 시작할 수 있는 토큰. *)
(* 세 조건이 다 필요하다. 본문 안의 fn을 새 항목으로 오인하면 그 뒤가 전부 *)
(* 어긋난다. *)
let item_starts =
[
Token.Kw_import;
Token.Kw_reexport;
Token.Kw_pub;
Token.Kw_fn;
Token.Kw_struct;
Token.Kw_enum;
Token.Kw_capability;
Token.Kw_const;
Token.Kw_copyable;
]
let at_line_start st = st.i > 0 && st.toks.(st.i - 1).Token.kind = Token.Newline
let sync st =
let depth = ref 0 in
let fin = ref false in
while not !fin do
match kind st with
| Token.Eof -> fin := true
| Token.LBrace ->
incr depth;
adv st
| Token.RBrace ->
decr depth;
adv st
| k ->
if !depth <= 0 && at_line_start st && List.mem k item_starts then
fin := true
else adv st
done
let parse_module st =
skip_nl st;
let errors = ref [] in
let rec loop acc =
if kind st = Token.Eof then List.rev acc
else
let it = parse_item st in
skip_nl st;
loop (it :: acc)
match parse_item st with
| it ->
skip_nl st;
loop (it :: acc)
| exception Error e ->
errors := e :: !errors;
let before = st.i in
sync st;
(* 진행 보장. 같은 자리에서 다시 실패하면 무한 루프다. *)
if st.i = before then adv st;
skip_nl st;
loop acc
in
{ items = loop [] }
let items = loop [] in
({ items }, List.rev !errors)
let parse (tokens : Token.t list) : modul =
let parse_all (tokens : Token.t list) : modul * error list =
let st = { toks = Array.of_list tokens; i = 0; no_struct = false } in
parse_module st
let parse (tokens : Token.t list) : modul =
match parse_all tokens with m, [] -> m | _, e :: _ -> raise (Error e)
let parse_result tokens =
match parse tokens with m -> Ok m | exception Error e -> Error e
match parse_all tokens with m, [] -> Ok m | _, e :: _ -> Error e
+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)
+55 -12
View File
@@ -11,7 +11,10 @@
open Ast
type error = { pos : Token.pos; msg : string }
(* blocking: 이름을 해소하지 못했다는 뜻이고, 이후 단계는 의미가 없다.
lint는 다르다 — 코드는 분석 가능하고 검사도 계속되어야 한다.
둘을 같은 통에 넣으면 lint 하나가 진짜 타입 오류를 가린다. *)
type error = { pos : Token.pos; msg : string; blocking : bool }
type info = { externals : (string * Token.pos) list }
type item_kind = K_fn | K_type | K_const | K_import
@@ -23,13 +26,20 @@ type state = {
mutable scopes : (string * bool) list list; (* 이름 -> 가변 여부 *)
mutable errors : error list;
mutable ext : (string * Token.pos) list;
(* 실제로 참조된 import 별칭 *)
used : (string, unit) Hashtbl.t;
}
let builtin_types =
[ "Int"; "Bool"; "String"; "Unit"; "List"; "Option"; "Result" ]
let builtin_values = [ "unit"; "Ok"; "Err"; "Some"; "None" ]
let error st pos msg = st.errors <- { pos; msg } :: st.errors
let error st pos msg = st.errors <- { pos; msg; blocking = true } :: st.errors
let lint st pos msg = st.errors <- { pos; msg; blocking = false } :: st.errors
let use_alias st a =
if Hashtbl.find_opt st.items a = Some K_import then
Hashtbl.replace st.used a ()
let external_ref st name pos =
if not (List.mem_assoc name st.ext) then st.ext <- (name, pos) :: st.ext
@@ -77,7 +87,13 @@ let resolve_eff_atom st = function
names
let rec resolve_ty st = function
| T_named { name; args; pos } ->
(* 한정된 이름은 별칭이 이 모듈에 있는지만 본다. 그 모듈 안에 그 타입이
있는지는 모듈 하나만 보고 결정할 수 없다 — 외부 참조로 기록한다. *)
| T_named { modl = Some a; args; pos; _ } ->
use_alias st a;
if Hashtbl.find_opt st.items a <> Some K_import then external_ref st a pos;
List.iter (resolve_targ st pos) args
| T_named { modl = None; name; args; pos } ->
if
(not (List.mem name st.ty_params))
&& (not (List.mem name builtin_types))
@@ -88,7 +104,7 @@ let rec resolve_ty st = function
then external_ref st name pos;
List.iter (resolve_targ st pos) args
| 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));
Option.iter (resolve_ty st) ret
@@ -118,7 +134,11 @@ let rec resolve_pattern st seen = function
else (
seen := n :: !seen;
bind st pos n false))
| P_ctor { name; args; pos } ->
| P_ctor { modl = Some a; args; pos; _ } ->
use_alias st a;
if Hashtbl.find_opt st.items a <> Some K_import then external_ref st a pos;
List.iter (resolve_pattern st seen) args
| P_ctor { modl = None; name; args; pos } ->
(match Hashtbl.find_opt st.ctors name with
| Some (enum, arity) when arity <> List.length args ->
error st pos
@@ -134,6 +154,7 @@ let rec resolve_pattern st seen = function
let rec resolve_expr st = function
| E_lit _ -> ()
| E_crash { msg; _ } -> resolve_expr st msg
| E_ident (n, pos) ->
if lookup_local st n = None then
if Hashtbl.mem st.items n then ()
@@ -144,14 +165,14 @@ let rec resolve_expr st = function
else external_ref st n pos
| E_list (xs, _) -> List.iter (resolve_expr st) xs
| E_struct { name; fields; pos } ->
resolve_ty st (T_named { name; args = []; pos });
resolve_ty st (T_named { modl = None; name; args = []; pos });
List.iter (fun (_, e) -> resolve_expr st e) fields
| E_closure c ->
push st;
List.iter
(fun (n, t) ->
Option.iter (resolve_ty st) t;
bind st c.cl_pos n false)
(fun (p : cl_param) ->
Option.iter (resolve_ty st) p.cp_ty;
bind st c.cl_pos p.cp_name false)
c.cl_params;
(match c.cl_eff with
| None -> ()
@@ -191,7 +212,12 @@ let rec resolve_expr st = function
| E_call { callee; args; _ } ->
resolve_expr st callee;
List.iter (resolve_expr st) args
| E_field { obj; _ } -> resolve_expr st obj
| E_field { obj; _ } ->
(* Alias.f — 별칭 접근도 사용이다 *)
(match obj with
| E_ident (o, _) when lookup_local st o = None -> use_alias st o
| _ -> ());
resolve_expr st obj
| E_inst { callee; args; pos } ->
resolve_expr st callee;
List.iter (resolve_targ st pos) args
@@ -236,7 +262,7 @@ and resolve_stmt st = function
| _ -> None
in
match root place with
| None -> ()
| None -> error st pos "대입 왼쪽에는 변수나 필드만 올 수 있습니다"
| Some n -> (
match lookup_local st n with
| Some true -> ()
@@ -304,6 +330,7 @@ let resolve (m : modul) : info * error list =
scopes = [];
errors = [];
ext = [];
used = Hashtbl.create 8;
}
in
(* 1차: 모듈 수준 이름을 모은다. 선언 순서에 의존하지 않는다. *)
@@ -324,7 +351,7 @@ let resolve (m : modul) : info * error list =
variants
| I_capability { name; pos; _ } -> declare st pos name K_type
| I_const { name; pos; _ } -> declare st pos name K_const
| I_reexport _ -> ())
| I_test _ | I_reexport _ -> ())
m.items;
(* 2차: 본문을 훑는다. *)
List.iter
@@ -352,8 +379,24 @@ let resolve (m : modul) : info * error list =
resolve_ty st ty;
push st;
resolve_expr st value;
pop st
(* 테스트는 파라미터 없는 본문이다. 나머지는 함수와 같다 *)
| I_test { body; _ } ->
push st;
resolve_block st body;
pop st)
m.items;
(* 미사용 import는 오류다. 취향 문제가 아니라 invalidation 표면 문제다 —
쓰지도 않는 모듈의 시그니처가 바뀌면 이 모듈이 재검사된다. 증분 루프의
비용을 아무 이유 없이 넓히는 선언은 남겨둘 수 없다. *)
List.iter
(fun it ->
match it with
| I_import { alias; pos; _ } when not (Hashtbl.mem st.used alias) ->
lint st pos
(Printf.sprintf "%s을(를) 가져왔지만 쓰지 않습니다 (재검사 범위만 넓힙니다)" alias)
| _ -> ())
m.items;
let by_pos (_, a) (_, b) =
compare (a.Token.line, a.Token.col) (b.Token.line, b.Token.col)
in
+304
View File
@@ -0,0 +1,304 @@
(* 모듈 로딩, interface 캐시, 그리고 고정점 invalidation.
여기가 v0가 존재하는 이유다. 검사 자체보다 "무엇을 다시 검사해야 하는가"를
좁게 유지하는 것이 아키텍처의 주장이고, 그 주장은 측정으로만 증명된다.
전파는 고정점 규칙이다 (문서 P8):
1. 변경된 모듈 자체를 재검사
2. 재검사 전후의 interface hash를 비교
3. 달라졌을 때만 그 모듈의 dependents를 큐에 추가
4. 큐가 빌 때까지 반복
순서가 중요하다. dependents를 먼저 재검사하면 "본문만 수정 시 downstream
0건"이 성립하지 않는다 — hash 비교가 dependents 재검사보다 앞서야 한다. *)
type error = { file : string; line : int; col : int; message : string }
let string_of_error { file; line; col; message } =
Printf.sprintf "%s:%d:%d: %s" file line col message
type entry = {
path : string;
ast : Ast.modul;
imports : (string * string) list; (* 별칭 -> 해소된 경로 *)
iface : Iface.t;
errors : error list;
}
type t = {
root : string;
std : string option; (* 표준 라이브러리 디렉터리 *)
modules : (string, entry) Hashtbl.t;
(* 통계: 무엇이 몇 번 재검사됐는지. 측정이 목적이므로 처음부터 센다. *)
mutable checked : string list;
}
(* 표준 라이브러리를 찾는다. COOL_STD가 있으면 그것, 없으면 위로 올라가며
std/list.cool을 찾는다. 못 찾으면 없는 것이고, 그때 std 이름들은 외부
참조로 남는다 — 없다고 말하지 않는다. *)
let find_std root =
match Sys.getenv_opt "COOL_STD" with
| Some d when Sys.file_exists d -> Some d
| _ ->
let rec up dir n =
if n = 0 then None
else
let cand = Filename.concat dir "std" in
if Sys.file_exists (Filename.concat cand "list.cool") then Some cand
else
let parent = Filename.dirname dir in
if parent = dir then None else up parent (n - 1)
in
up
(if Filename.is_relative root then Filename.concat (Sys.getcwd ()) root
else root)
8
let create ?(root = ".") ?std () =
{
root;
std = (match std with Some _ -> std | None -> find_std root);
modules = Hashtbl.create 16;
checked = [];
}
(* 패키지 경로와 지역 모듈 경로를 구분한다. 첫 세그먼트에 점이 있으면
패키지 참조다 (cool.dev/std/list). v0에는 패키지 해소가 없으므로 그런
import는 불투명하게 남는다 — 없다고 말하지 않는다. *)
let is_package path =
(* ./ 와 ../ 로 시작하면 상대 경로다. 첫 세그먼트에 점이 있다는 것만 보면
".."이 패키지로 오인된다 — 그러면 import가 조용히 해소되지 않고, 그
모듈의 이름이 전부 불투명해져 검사가 통째로 공허해진다. *)
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 starts_with p s =
String.length s >= String.length p && String.sub s 0 (String.length p) = p
(* 경로를 파일로 바꾼다. 바꿀 수 없으면 None — v0에는 패키지 해소가 없으므로
표준 라이브러리 밖의 패키지는 불투명하게 남는다. *)
let resolve_import st path : string option =
if starts_with std_prefix path then
let name =
String.sub path (String.length std_prefix)
(String.length path - String.length std_prefix)
in
match st.std with
| Some d -> Some (Filename.concat d (name ^ ".cool"))
| None -> None
else if is_package path then None
else Some (Filename.concat st.root (path ^ ".cool"))
let read_file 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 err_of file (pos : Token.pos) msg =
{ file; line = pos.line; col = pos.col; message = msg }
(* 파서는 항목 단위로 회복하므로 오류가 여럿일 수 있다. 첫 오류에서 멈추면
고칠 때마다 다시 돌려야 하고, 그것이 빠른 루프의 반대다. *)
let parse_file file =
match Lexer.lex_result (read_file file) with
| Error e -> Error [ err_of file e.pos e.msg ]
| Ok toks -> (
match Parser.parse_all toks with
| m, [] -> Ok m
| _, errs ->
Error
(List.map (fun (e : Parser.error) -> err_of file e.pos e.msg) errs))
let imports_of (m : Ast.modul) =
List.filter_map
(function
| Ast.I_import { path; alias; _ } -> Some (alias, path) | _ -> None)
m.items
(* 한 모듈을 검사한다. 의존 모듈의 interface는 이미 로드되어 있어야 한다. *)
let check_module st path : entry =
st.checked <- path :: st.checked;
match parse_file path with
| Error es ->
{
path;
ast = { items = [] };
imports = [];
iface = { items = []; hash = "" };
errors = es;
}
| Ok ast ->
let imports =
List.filter_map
(fun (a, p) ->
match resolve_import st p with
| Some f -> Some (a, f)
| None -> None)
(imports_of ast)
in
(* 의존 모듈의 exported surface를 소비 측 별칭으로 한정해 합친다.
여기서부터 검사기는 "이 모듈 + 아는 외부 표면"만 본다. *)
let dep_surface =
List.concat_map
(fun (alias, p) ->
match Hashtbl.find_opt st.modules p with
| Some e -> Iface.qualify alias e.iface.Iface.items
| None -> [])
imports
in
let iface = Iface.of_module ast in
let _, rerrors = Resolve.resolve ast in
(* 이름을 해소하지 못했으면 뒤 단계는 의미가 없다. lint는 막지 않는다. *)
let blocking =
List.filter (fun (e : Resolve.error) -> e.blocking) rerrors
in
let errors =
List.map (fun (e : Resolve.error) -> err_of path e.pos e.msg) rerrors
in
let errors =
if blocking <> [] then errors
else
errors
@ List.map
(fun (e : Typecheck.error) -> err_of path e.pos e.msg)
(Typecheck.check ~imports:dep_surface ast)
@ List.map
(fun (e : Move.error) -> err_of path e.pos e.msg)
(Move.check ~imports:dep_surface ast)
in
let errors =
List.sort (fun a b -> compare (a.line, a.col) (b.line, b.col)) errors
in
{ path; ast; imports; iface; errors }
(* 의존 순서대로 로드한다. 순환은 오류다. *)
let rec load st ?(visiting = []) path : unit =
if Hashtbl.mem st.modules path then ()
else if List.mem path visiting then ()
else if not (Sys.file_exists path) then
Hashtbl.replace st.modules path
{
path;
ast = { items = [] };
imports = [];
iface = { items = []; hash = "" };
errors =
[ { file = path; line = 0; col = 0; message = "모듈을 찾을 수 없습니다" } ];
}
else begin
(match parse_file path with
| Error _ -> ()
| Ok ast ->
List.iter
(fun (_, p) ->
match resolve_import st p with
| Some f -> load st ~visiting:(path :: visiting) f
| None -> ())
(imports_of ast));
Hashtbl.replace st.modules path (check_module st path)
end
let dependents st path =
Hashtbl.fold
(fun p e acc ->
if List.exists (fun (_, d) -> d = path) e.imports then p :: acc else acc)
st.modules []
(* 고정점 전파. 반환값은 실제로 재검사한 모듈 목록이다. *)
let recheck st (changed : string list) : string list =
st.checked <- [];
let queue = ref changed in
let seen = Hashtbl.create 8 in
while !queue <> [] do
let path = List.hd !queue in
queue := List.tl !queue;
if not (Hashtbl.mem seen path) then begin
Hashtbl.replace seen path ();
let before =
match Hashtbl.find_opt st.modules path with
| Some e -> e.iface.Iface.hash
| None -> ""
in
let entry = check_module st path in
Hashtbl.replace st.modules path entry;
(* hash 비교가 dependents 재검사보다 앞선다 *)
if entry.iface.Iface.hash <> before then
queue := !queue @ dependents st path
end
done;
List.rev st.checked
let errors st =
Hashtbl.fold (fun _ e acc -> e.errors @ acc) st.modules []
|> List.sort (fun a b ->
compare (a.file, a.line, a.col) (b.file, b.line, b.col))
let find st path = Hashtbl.find_opt st.modules path
(* ------------------------------------------------------------------ *)
(* 실행 *)
(* ------------------------------------------------------------------ *)
(* main이 선언한 파라미터의 타입 이름을 뽑는다. 런타임은 이 목록만 보고
권한을 만든다 — 선언하지 않은 capability는 프로그램에 존재하지 않는다. *)
let main_params (m : Ast.modul) =
let rec ty_name (t : Ast.ty) =
match t with
| Ast.T_named { modl; name; _ } -> (
match modl with Some a -> a ^ "." ^ name | None -> name)
| Ast.T_fn _ -> "<fn>"
in
List.concat_map
(function
| Ast.I_fn { decl; _ } when decl.fn_name = "main" ->
List.map
(fun (p : Ast.param) -> (p.p_name, ty_name p.p_ty))
decl.fn_params
| _ -> [])
m.items
(* 출력과 실패를 함께 돌려준다. 실패해도 그때까지 나온 것은 보여줘야 한다 *)
let run ?(args = []) st path : string * error option =
load st path;
let errs = errors st in
if errs <> [] then ("", Some (List.hd errs))
else
match find st path with
| None ->
("", Some { file = path; line = 0; col = 0; message = "모듈을 찾을 수 없습니다" })
| Some e -> (
(* IR은 그래프 전체를 받는다. 별칭이 실행 의미에 남지 않도록. *)
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
let prog = Ir.of_program mods in
match Interp.run ~args prog path (main_params e.ast) with
| out, None -> (out, None)
| 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))
+77
View File
@@ -28,6 +28,8 @@ type kind =
| Kw_else
| Kw_match
| Kw_scope
| Kw_crash
| Kw_test
| Kw_true
| Kw_false
(* 구두점 *)
@@ -87,6 +89,8 @@ let keyword = function
| "else" -> Some Kw_else
| "match" -> Some Kw_match
| "scope" -> Some Kw_scope
| "crash" -> Some Kw_crash
| "test" -> Some Kw_test
| "true" -> Some Kw_true
| "false" -> Some Kw_false
| _ -> None
@@ -115,6 +119,8 @@ let show_kind = function
| Kw_else -> "else"
| Kw_match -> "match"
| Kw_scope -> "scope"
| Kw_crash -> "crash"
| Kw_test -> "test"
| Kw_true -> "true"
| Kw_false -> "false"
| LParen -> "("
@@ -152,6 +158,77 @@ let show_kind = function
let show { kind; pos } =
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_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 어휘 절).
값으로 끝날 수 있는 토큰 뒤에서만 삽입하므로, 연산자나 여는 괄호로
끝나는 줄은 다음 줄로 이어진다. *)
+383 -63
View File
@@ -12,8 +12,11 @@ module T = Types
type error = { pos : Token.pos; msg : string }
type scheme = {
s_gen : string list; (* 타입 파라미터 이름 (effect 파라미터는 제외) *)
s_params : T.t list;
s_gen : string list; (* 타입 파라미터 *)
s_eff_gen : string list; (* effect 파라미터 *)
(* (own, 타입). 무표기는 빌림 *)
s_params : (bool * T.t) list;
s_eff : T.eff; (* 이 함수를 부르면 수행되는 effect *)
s_ret : T.t;
}
@@ -24,13 +27,43 @@ type env = {
fns : (string, scheme) Hashtbl.t;
consts : (string, T.t) Hashtbl.t;
ctors : (string, string) Hashtbl.t; (* variant -> enum *)
(* 가져온 모듈의 별칭. `Alias.x`는 "Alias.x"라는 하나의 키로 찾는다 —
별칭은 식별자에 쓸 수 없는 점(.)을 포함하므로 지역 이름과 충돌하지 않는다. *)
aliases : string list;
mutable locals : (string * T.t) list list;
mutable ret : T.t; (* 현재 함수의 선언된 반환 타입 *)
(* 현재 본문이 수행한 effect. 위치를 같이 들고 다녀야 "어디서 수행했는지"를
말할 수 있다. 클로저에 들어가면 저장하고 비운다 — 클로저의 effect는
정의한 자리가 아니라 부르는 자리에서 일어난다. *)
mutable performed : (T.atom * Token.pos) list;
(* 이 본문에서 모르는 것을 만났는가. 과잉 선언 판정에만 쓴다 — 외부 타입의
메서드는 effect를 알 수 없으므로 "수행하지 않았다"고 말할 근거가 없다. *)
mutable saw_unknown : bool;
mutable errors : error list;
}
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 =
err env pos
(Printf.sprintf "%s: %s이(가) 필요한데 %s입니다" what (T.show expected) (T.show got))
@@ -55,17 +88,26 @@ let lookup env n =
(* Ast.ty -> Types.t *)
(* ------------------------------------------------------------------ *)
let conv_eff_atom (a : Ast.eff_atom) : T.eff =
match a with
| Eff_var v -> [ T.A_var v ]
| Eff_set names -> List.map (fun { cap; meth } -> T.A_name (cap, meth)) names
let conv_eff_result (atoms : Ast.eff_result) : T.eff =
T.eff_resolve (List.concat_map conv_eff_atom atoms)
let rec conv env (gen : string list) (t : Ast.ty) : T.t =
match t with
| T_named { name; args; _ } -> (
| T_named { modl; name; args; _ } -> (
let args =
List.filter_map
(function TA_ty t -> Some (conv env gen t) | TA_eff _ -> None)
args
in
if List.mem name gen then T.TVar name
let name = match modl with Some a -> a ^ "." ^ name | None -> name in
if modl = None && List.mem name gen then T.TVar name
else
match name with
match if modl = None then name else "" with
| "Int" -> T.TInt
| "Bool" -> T.TBool
| "String" -> T.TString
@@ -77,11 +119,15 @@ let rec conv env (gen : string list) (t : Ast.ty) : T.t =
|| Hashtbl.mem env.enums name || Hashtbl.mem env.caps name
then T.TCon (name, args)
else T.TUnknown)
| T_fn { affine; params; ret; _ } ->
| T_fn { affine; params; eff; ret; _ } ->
T.TFn
{
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);
ret = (match ret with None -> T.TUnit | Some r -> conv env gen r);
}
@@ -91,20 +137,34 @@ let scheme_of env (d : fn_decl) : scheme =
(fun g -> if g.gp_effect then None else Some g.gp_name)
d.fn_gen
in
let egen =
List.filter_map
(fun g -> if g.gp_effect then Some g.gp_name else None)
d.fn_gen
in
{
s_gen = gen;
s_params = List.map (fun p -> conv env gen p.p_ty) d.fn_params;
s_eff_gen = egen;
s_params = List.map (fun p -> (p.p_own, conv env gen p.p_ty)) d.fn_params;
s_eff =
(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);
}
(* 호출 지점 인스턴스화: 타입 파라미터마다 새 미지수 *)
let instantiate (s : scheme) =
let sub = List.map (fun v -> (v, T.fresh ())) s.s_gen in
(List.map (T.subst sub) s.s_params, T.subst sub s.s_ret)
let esub = List.map (fun v -> (v, T.fresh_eff ())) s.s_eff_gen in
( List.map (fun (o, t) -> (o, T.subst sub esub t)) s.s_params,
T.subst_eff esub s.s_eff,
T.subst sub esub s.s_ret )
let instantiate_with (s : scheme) (args : T.t list) =
let sub = List.map2 (fun v a -> (v, a)) s.s_gen args in
(List.map (T.subst sub) s.s_params, T.subst sub s.s_ret)
let esub = List.map (fun v -> (v, T.fresh_eff ())) s.s_eff_gen in
( List.map (fun (o, t) -> (o, T.subst sub esub t)) s.s_params,
T.subst_eff esub s.s_eff,
T.subst sub esub s.s_ret )
(* ------------------------------------------------------------------ *)
(* 내장 생성자 *)
@@ -128,6 +188,12 @@ let builtin_ctor = function
let rec infer env (e : expr) : T.t =
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_str _, _) -> T.TString
| E_lit (L_bool _, _) -> T.TBool
@@ -144,12 +210,14 @@ let rec infer env (e : expr) : T.t =
| _ -> (
match Hashtbl.find_opt env.fns n with
| Some s ->
let params, ret = instantiate s in
T.TFn { affine = false; params; ret }
let params, eff, ret = instantiate s in
T.TFn { affine = false; params; eff; ret }
| None -> (
match Hashtbl.find_opt env.ctors n with
| Some enum -> nullary_ctor env enum n
| None -> T.TUnknown)))))
| None ->
env.saw_unknown <- true;
T.TUnknown)))))
| E_list (xs, pos) ->
let elem = T.fresh () in
List.iter
@@ -187,6 +255,7 @@ let rec infer env (e : expr) : T.t =
pop env)
arms;
if arms = [] then err env pos "match에 팔이 없습니다";
check_exhaustive env s arms pos;
result
| E_scope { name; parent; body; pos } ->
(match lookup env parent with
@@ -247,6 +316,40 @@ let rec infer env (e : expr) : T.t =
if not (T.unify a b) then mismatch env pos a b "같은 타입끼리만 비교할 수 있습니다";
T.TBool)
(* exhaustiveness: 철학 1의 대표 항목이자 interface hash가 enum 본문을
입력으로 삼는 이유다 *)
and check_exhaustive env scrutinee arms pos =
let eenv : Exhaust.env =
{
variants =
(fun name args ->
match Hashtbl.find_opt env.enums name with
| None -> None
| Some (gen, variants) ->
let sub =
try List.map2 (fun v a -> (v, a)) gen args
with Invalid_argument _ -> []
in
Some
(List.map
(fun (n, tys) -> (n, List.map (T.subst sub []) tys))
variants));
is_ctor =
(fun n ->
Hashtbl.mem env.ctors n || List.mem n [ "Ok"; "Err"; "Some"; "None" ]);
}
in
let r = Exhaust.check eenv scrutinee (List.map (fun a -> a.arm_pat) arms) in
(match r.missing with
| None -> ()
| Some w -> err env pos (Printf.sprintf "match가 모든 경우를 덮지 않습니다 (빠진 경우: %s)" w));
List.iter
(fun i ->
match List.nth_opt arms i with
| Some a -> err env a.arm_pos "이 팔은 앞의 팔들에 가려 도달할 수 없습니다"
| None -> ())
r.unreachable
and nullary_ctor env enum name =
match Hashtbl.find_opt env.enums enum with
| None -> T.TUnknown
@@ -268,7 +371,7 @@ and infer_struct env name fields pos =
match List.assoc_opt fname decl_fields with
| None -> err env pos (Printf.sprintf "%s에 %s 필드가 없습니다" name fname)
| Some ft ->
let want = T.subst sub ft in
let want = T.subst sub [] ft in
let got = infer env fe in
if not (T.unify want got) then
mismatch env pos want got (Printf.sprintf "%s.%s 필드" name fname))
@@ -285,36 +388,69 @@ and infer_closure env (c : closure) (expected : T.t option) =
match Option.map T.resolve expected with
| Some (T.TFn { params; ret; _ })
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)
in
push env;
let param_tys =
List.map2
(fun (n, ann) exp ->
(fun (p : cl_param) exp ->
let t =
match ann with
match p.cp_ty with
| 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
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
in
let declared_ret = Option.map (conv env []) c.cl_ret in
let saved = env.ret in
let saved_ret = env.ret in
env.ret <-
(match declared_ret with
| Some t -> t
| None -> ( match expected_ret with Some t -> t | None -> T.TUnknown));
(* 클로저 본문의 effect는 바깥 함수가 수행하는 것이 아니다. 저장하고 비운다. *)
let saved_perf = env.performed in
env.performed <- [];
let body = infer_block env c.cl_body in
let inner = env.performed in
env.performed <- saved_perf;
let declared_eff = Option.map conv_eff_atom c.cl_eff in
let eff =
match declared_eff with
| Some d ->
(* 클로저가 effects를 명시했으면 본문이 그 안에 들어야 한다 *)
List.iter
(fun (a, pos) ->
List.iter
(fun m ->
err env pos
(Printf.sprintf "클로저가 선언하지 않은 effect %s을(를) 수행합니다 (선언: %s)"
(T.atom_show m) (T.eff_show d)))
(T.eff_missing ~declared:d ~performed:[ a ]))
inner;
d
| None -> T.eff_resolve (List.map fst inner)
in
(match declared_ret with
| Some t when not (T.unify t body) -> mismatch env c.cl_pos t body "클로저의 반환"
| _ -> ());
let ret = match declared_ret with Some t -> t | None -> body in
env.ret <- saved;
env.ret <- saved_ret;
pop env;
T.TFn { affine = false; params = param_tys; ret }
T.TFn { affine = false; params = param_tys; eff; ret }
and infer_call env callee args pos =
let fn_ty =
@@ -324,12 +460,20 @@ and infer_call env callee args pos =
| Some enum -> Some (ctor_fn env enum n)
| None -> (
match builtin_ctor n with
| Some (params, ret) -> Some (T.TFn { affine = false; params; ret })
| Some (params, ret) ->
Some
(T.TFn
{
affine = false;
params = List.map (fun t -> (false, t)) params;
eff = [];
ret;
})
| None -> (
match Hashtbl.find_opt env.fns n with
| Some s ->
let params, ret = instantiate s in
Some (T.TFn { affine = false; params; ret })
let params, eff, ret = instantiate s in
Some (T.TFn { affine = false; params; eff; ret })
| None -> None)))
| _ -> (
match T.resolve (infer env callee) with
@@ -338,9 +482,13 @@ and infer_call env callee args pos =
in
match fn_ty with
| None ->
env.saw_unknown <- true;
List.iter (fun a -> ignore (infer env a)) args;
T.TUnknown
| Some (T.TFn { params; ret; _ }) ->
| Some (T.TFn { params; eff; ret; _ }) ->
List.iter
(fun a -> env.performed <- (a, pos) :: env.performed)
(T.eff_resolve eff);
if List.length params <> List.length args then (
err env pos
(Printf.sprintf "인자 %d개가 필요한데 %d개가 주어졌습니다" (List.length params)
@@ -348,13 +496,28 @@ and infer_call env callee args pos =
List.iter (fun a -> ignore (infer env a)) args)
else
List.iter2
(fun p a ->
(fun (_, p) a ->
let got =
match a with
| E_closure c -> infer_closure env c (Some p)
| _ -> infer env a
in
if not (T.unify p got) then mismatch env pos p got "인자")
(* 먼저 unify한다. 결정 위치의 effect 변수는 여기서 인자의
effect로 묶인다 — 제약이 아니라 해소다. 그 뒤에 남는 차이만이
진짜 위반이다. *)
if not (T.unify p got) then mismatch env pos p got "인자";
match (T.resolve p, T.resolve got) with
| T.TFn pf, T.TFn gf ->
let missing =
T.eff_missing ~declared:pf.eff ~performed:gf.eff
in
if missing <> [] then
err env pos
(Printf.sprintf
"넘긴 함수가 %s을(를) 수행하는데 파라미터가 허용한 effect는 %s입니다"
(String.concat ", " (List.map T.atom_show missing))
(T.eff_show pf.eff))
| _ -> ())
params args;
ret
| Some _ -> T.TUnknown
@@ -366,39 +529,79 @@ and ctor_fn env enum name =
let sub = List.map (fun v -> (v, T.fresh ())) gen in
let params =
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 -> []
in
T.TFn { affine = false; params; ret = T.TCon (enum, List.map snd sub) }
T.TFn
{
affine = false;
params;
eff = [];
ret = T.TCon (enum, List.map snd sub);
}
and infer_field env obj name pos =
let t = infer env obj in
match T.resolve t with
| T.TUnknown -> T.TUnknown
| T.TCon (cname, args) -> (
match Hashtbl.find_opt env.structs cname with
| Some (gen, fields) -> (
let sub = List.map2 (fun v a -> (v, a)) gen (adjust gen args) in
match List.assoc_opt name fields with
| Some ft -> T.subst sub ft
| None ->
err env pos (Printf.sprintf "%s에 %s 필드가 없습니다" cname name);
T.TUnknown)
(* capability 메서드는 값을 통해서만 부를 수 있다. 타입 이름으로 부를 수 있으면
capability 없이 effect를 수행하게 되어 보안 정리 (i)이 무너진다. *)
(match obj with
| E_ident (n, _) when lookup env n = None && Hashtbl.mem env.caps n ->
err env pos
(Printf.sprintf
"capability %s의 메서드는 값을 통해서만 부를 수 있습니다 (%s를 파라미터로 받아야 합니다)" n n)
| _ -> ());
match obj with
| E_ident (a, _) when lookup env a = None && List.mem a env.aliases -> (
(* 모듈 별칭을 통한 접근. 가져온 표면은 "Alias.name" 키로 들어와 있다. *)
let key = a ^ "." ^ name in
let unknown () =
err env pos (Printf.sprintf "%s에 %s이(가) 없습니다" a name);
T.TUnknown
in
match Hashtbl.find_opt env.consts key with
| Some t -> t
| None -> (
match Hashtbl.find_opt env.caps cname with
| Some methods -> (
match List.assoc_opt name methods with
| Some s ->
let params, ret = instantiate s in
T.TFn { affine = false; params; ret }
match Hashtbl.find_opt env.fns key with
| Some s ->
let params, eff, ret = instantiate s in
T.TFn { affine = false; params; eff; ret }
| None -> (
match Hashtbl.find_opt env.ctors key with
| Some enum -> (
match Hashtbl.find_opt env.enums enum with
| Some (_, variants)
when List.assoc_opt key variants = Some [] ->
nullary_ctor env enum key
| Some _ -> ctor_fn env enum key
| None -> T.TUnknown)
| None -> unknown ())))
| _ -> (
let t = infer env obj in
match T.resolve t with
| T.TUnknown -> T.TUnknown
| T.TCon (cname, args) -> (
match Hashtbl.find_opt env.structs cname with
| Some (gen, fields) -> (
let sub = List.map2 (fun v a -> (v, a)) gen (adjust gen args) in
match List.assoc_opt name fields with
| Some ft -> T.subst sub [] ft
| None ->
err env pos
(Printf.sprintf "capability %s에 %s 메서드가 없습니다" cname name);
err env pos (Printf.sprintf "%s에 %s 필드가 없습니다" cname name);
T.TUnknown)
| None -> T.TUnknown))
| other ->
err env pos (Printf.sprintf "%s에는 필드가 없습니다" (T.show other));
T.TUnknown
| None -> (
match Hashtbl.find_opt env.caps cname with
| Some methods -> (
match List.assoc_opt name methods with
| Some s ->
let params, eff, ret = instantiate s in
T.TFn { affine = false; params; eff; ret }
| None ->
err env pos
(Printf.sprintf "capability %s에 %s 메서드가 없습니다" cname name);
T.TUnknown)
| None -> T.TUnknown))
| other ->
err env pos (Printf.sprintf "%s에는 필드가 없습니다" (T.show other));
T.TUnknown)
and adjust gen args =
let n = List.length gen in
@@ -428,8 +631,8 @@ and infer_inst env callee args pos =
(List.length s.s_gen) (List.length tys));
T.TUnknown)
else
let params, ret = instantiate_with s tys in
T.TFn { affine = false; params; ret })
let params, eff, ret = instantiate_with s tys in
T.TFn { affine = false; params; eff; ret })
| _ -> T.TUnknown
and check_pattern env (scrutinee : T.t) (p : pattern) =
@@ -448,7 +651,8 @@ and check_pattern env (scrutinee : T.t) (p : pattern) =
| Some enum ->
check_ctor env scrutinee enum n [] Token.{ line = 0; col = 0 }
| None -> bind env n scrutinee)
| P_ctor { name; args; pos } -> (
| P_ctor { modl; name; args; pos } -> (
let name = match modl with Some a -> a ^ "." ^ name | None -> name in
match Hashtbl.find_opt env.ctors name with
| Some enum -> check_ctor env scrutinee enum name args pos
| None -> List.iter (check_pattern env T.TUnknown) args)
@@ -466,7 +670,7 @@ and check_ctor env scrutinee enum name args pos =
in
if List.length fields = List.length args then
List.iter2
(fun ft ap -> check_pattern env (T.subst sub ft) ap)
(fun ft ap -> check_pattern env (T.subst sub [] ft) ap)
fields args
(* ------------------------------------------------------------------ *)
@@ -484,6 +688,17 @@ and infer_block env (b : block) : T.t =
go b.stmts
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; _ } ->
let declared = Option.map (conv env []) ty in
let got =
@@ -506,7 +721,6 @@ and check_stmt env = function
let p = infer env place in
let v = infer env value in
if not (T.unify p v) then mismatch env pos p v "대입"
| S_expr e -> ignore (infer env e)
(* ------------------------------------------------------------------ *)
(* 모듈 *)
@@ -527,13 +741,63 @@ let check_fn env (d : fn_decl) =
match d.fn_ret with None -> T.TUnit | Some r -> conv env gen r
in
env.ret <- declared;
env.performed <- [];
env.saw_unknown <- false;
let got = infer_block env body in
if not (T.unify declared got) then
mismatch env d.fn_pos declared got
(Printf.sprintf "%s의 본문이 남기는 값" d.fn_name);
(* 미선언 effect = compile error (철학 1).
수행한 자리를 알고 있으므로 그 자리에 진단을 붙인다. *)
let declared_eff =
match d.fn_eff with None -> [] | Some atoms -> conv_eff_result atoms
in
List.iter
(fun (a, pos) ->
match T.eff_missing ~declared:declared_eff ~performed:[ a ] with
| [] -> ()
| missing ->
List.iter
(fun m ->
err env pos
(Printf.sprintf "선언되지 않은 effect %s (%s의 effects 절은 %s입니다)"
(T.atom_show m) d.fn_name (T.eff_show declared_eff)))
missing)
(List.rev env.performed);
(* 과잉 선언도 오류다. 선언한 effect를 수행하지 않으면 호출자는 하지도
않는 일에 대한 의무를 진다 — 자기 effects 절을 넓히거나 capability를
받아오게 된다. 시그니처는 실제보다 좁아도 안 되고 넓어도 안 된다.
effect 변수가 있으면 판정하지 않는다. e에 무엇이 묶일지는 호출
지점이 정하고, 본문만 보고는 알 수 없다 — 모르는 것을 틀렸다고
말하지 않는다. *)
let has_var =
List.exists (function T.A_var _ -> true | _ -> false) declared_eff
in
if (not has_var) && not env.saw_unknown then
List.iter
(fun a ->
match a with
| T.A_name (cap, meth) ->
let performed = List.map fst env.performed |> T.eff_resolve in
if
not
(List.exists
(function
| T.A_name (c, m) -> c = cap && m = meth
| T.A_var _ | T.A_meta _ -> true)
performed)
then
err env d.fn_pos
(Printf.sprintf
"%s은(는) %s을(를) 선언했지만 수행하지 않습니다 (effects 절에서 지우십시오)"
d.fn_name (T.atom_show a))
| _ -> ())
declared_eff;
env.performed <- [];
pop env
let check (m : modul) : error list =
let check ?(imports : item list = []) (m : modul) : error list =
let env =
{
structs = Hashtbl.create 16;
@@ -542,11 +806,46 @@ let check (m : modul) : error list =
fns = Hashtbl.create 16;
consts = Hashtbl.create 16;
ctors = Hashtbl.create 16;
(* 표면을 실제로 받아온 별칭만 안다. 해소되지 않은 모듈(예: 아직 가져오지
못한 패키지)의 별칭은 모르는 것이므로 그 아래 이름을 틀렸다고 말하지
않는다. *)
aliases =
List.sort_uniq compare
(List.filter_map
(fun it ->
let n =
match it with
| I_fn { decl; _ } -> decl.fn_name
| I_struct { name; _ }
| I_enum { name; _ }
| I_capability { name; _ }
| I_const { name; _ } ->
name
| _ -> ""
in
match String.index_opt n '.' with
| Some i -> Some (String.sub n 0 i)
| None -> None)
imports);
locals = [];
ret = T.TUnit;
performed = [];
saw_unknown = false;
errors = [];
}
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차: 타입과 생성자 이름부터 (선언 순서에 의존하지 않는다) *)
List.iter
(fun it ->
@@ -559,7 +858,7 @@ let check (m : modul) : error list =
List.iter (fun v -> Hashtbl.replace env.ctors v.v_name name) variants
| I_capability { name; _ } -> Hashtbl.replace env.caps name []
| _ -> ())
m.items;
(imports @ m.items);
(* 2차: 본문을 채운다 *)
List.iter
(fun it ->
@@ -583,7 +882,7 @@ let check (m : modul) : error list =
| I_const { name; ty; _ } ->
Hashtbl.replace env.consts name (conv env [] ty)
| _ -> ())
m.items;
(imports @ m.items);
(* 3차: 본문 검사 *)
List.iter
(fun it ->
@@ -594,6 +893,27 @@ let check (m : modul) : error list =
let want = conv env [] ty in
let got = infer env value in
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;
List.sort
+122 -18
View File
@@ -1,37 +1,109 @@
(* 타입 표현과 지역 unification.
(* 타입과 effect 표현, 그리고 지역 unification.
TUnknown이 핵심이다. 외부 모듈에서 오는 이름은 모듈 로딩이 없는 v0에서
해소할 수 없다. 그런 타입은 TUnknown이 되고 무엇과도 맞는다 — 모르는 것을
틀렸다고 말하지 않기 위해서다. 아는 범위에서만 검사한다.
틀렸다고 말하지 않기 위해서다.
TMeta는 호출 지점에서 제네릭을 인스턴스화할 때 생기는 미지수다. 함수 하나
범위에서만 살고 전역으로 흐르지 않는다 (철학 2: 전역 추론 없음). *)
effect는 순서 없는 집합이고 합성은 합집합이다. 변수는 집합 변수이며,
호출 지점에서 결정 위치(파라미터의 effect 자리에 단독으로 선 변수)를 통해
메타에 묶인다. 함수 하나 범위를 넘지 않는다. *)
type t =
| TUnknown
(* 값을 내지 않는 타입. crash의 타입이고 어떤 자리에도 놓일 수 있다.
TUnknown과 다르다 — TUnknown은 "모른다"이고 TNever는 "돌아오지
않는다"이다. 둘 다 무엇과도 맞지만 생기는 이유가 다르다. *)
| TNever
| TInt
| TBool
| TString
| TUnit
| TVar of string
| TCon of string * t list
| TFn of { affine : bool; params : t list; ret : t }
(* params의 bool은 소유권이다 (own이면 true). affinity와 다른 축이다 —
affinity는 타입의 성질이고 소유권은 이 자리가 값을 가져가는가다. *)
| TFn of { affine : bool; params : (bool * t) list; eff : eff; ret : t }
| TMeta of meta ref
and meta = Unbound of int | Bound of t
(* effect 집합. 원소는 구체 이름, 집합 변수, 또는 호출 지점의 미지수다. *)
and eff = atom list
and atom =
| A_name of string * string (* Cap.method *)
| A_var of string
| A_meta of emeta ref
and emeta = EUnbound of int | EBound of eff
let counter = ref 0
let fresh () =
incr counter;
TMeta (ref (Unbound !counter))
let fresh_eff () =
incr counter;
[ A_meta (ref (EUnbound !counter)) ]
let rec resolve t =
match t with TMeta { contents = Bound u } -> resolve u | _ -> t
(* 묶인 메타를 펼치고 중복을 없앤다. 집합이므로 순서는 의미가 없다. *)
let rec eff_resolve (e : eff) : eff =
let expand a =
match a with
| A_meta { contents = EBound inner } -> eff_resolve inner
| _ -> [ a ]
in
let flat = List.concat_map expand e in
let mem a acc =
List.exists
(fun b ->
match (a, b) with
| A_name (c1, m1), A_name (c2, m2) -> c1 = c2 && m1 = m2
| A_var x, A_var y -> x = y
| A_meta r, A_meta r' -> r == r'
| _ -> false)
acc
in
List.fold_left (fun acc a -> if mem a acc then acc else a :: acc) [] flat
|> List.rev
let atom_show = function
| A_name (c, m) -> c ^ "." ^ m
| A_var v -> v
| A_meta { contents = EUnbound n } -> Printf.sprintf "_e%d" n
| A_meta { contents = EBound _ } -> "?"
let eff_show e =
match eff_resolve e with
| [] -> "{}"
| atoms -> "{" ^ String.concat ", " (List.map atom_show atoms) ^ "}"
let atom_eq a b =
match (a, b) with
| A_name (c1, m1), A_name (c2, m2) -> c1 = c2 && m1 = m2
| A_var x, A_var y -> x = y
| A_meta r, A_meta r' -> r == r'
| _ -> false
(* declared가 덮지 못하는 원소들. 미지수는 판정을 미룬다 —
결정되지 않은 것을 위반이라고 말하지 않는다. *)
let eff_missing ~declared ~performed =
let declared = eff_resolve declared in
List.filter
(fun a ->
match a with
| A_meta { contents = EUnbound _ } -> false
| _ -> not (List.exists (atom_eq a) declared))
(eff_resolve performed)
let rec show t =
match resolve t with
| TUnknown -> "?"
| TNever -> "Never"
| TInt -> "Int"
| TBool -> "Bool"
| TString -> "String"
@@ -39,10 +111,12 @@ let rec show t =
| TVar v -> v
| TCon (n, []) -> n
| TCon (n, args) -> n ^ "[" ^ String.concat ", " (List.map show args) ^ "]"
| TFn { affine; params; ret } -> (
| TFn { affine; params; eff; ret } -> (
(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 resolve ret with TUnit -> "" | r -> " -> " ^ show r)
| TMeta { contents = Unbound n } -> Printf.sprintf "_%d" n
| TMeta { contents = Bound _ } -> "?"
@@ -51,14 +125,28 @@ let rec occurs r t =
match resolve t with
| TMeta r' -> r == r'
| 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
(* 성공하면 true. 실패해도 예외를 던지지 않는다 — 호출자가 위치를 알고
진단을 만든다. *)
(* 결정 위치의 해소: 파라미터의 effect 자리에 단독으로 선 미지수만 묶는다.
그 외에는 참을 돌려주고, 실제 포함 검사는 호출 지점에서 방향을 아는
쪽이 한다 (진단 품질 때문에). *)
let unify_eff a b =
match (eff_resolve a, eff_resolve b) with
| [ A_meta ({ contents = EUnbound _ } as r) ], other ->
r := EBound other;
true
| other, [ A_meta ({ contents = EUnbound _ } as r) ] ->
r := EBound other;
true
| _ -> true
let rec unify a b =
match (resolve a, resolve b) with
| TUnknown, _ | _, TUnknown -> true
(* Never는 어떤 타입 자리에도 놓인다. crash가 match 팔에 설 수 있는 이유다 *)
| TNever, _ | _, TNever -> true
| TMeta r, TMeta r' when r == r' -> true
| TMeta r, t | t, TMeta r ->
if occurs r t then false
@@ -73,21 +161,37 @@ let rec unify a b =
(* affinity는 타입 동등성의 일부가 아니다. 값이 affine인지는 무엇을
capture했는지로 정해지는 substructural 성질이고, move/affinity 검사가
소유한다. 여기서 섞으면 두 검사가 서로의 결론을 앞질러 버린다. *)
(* 소유권은 타입 동등성의 일부다. 빌리는 클로저를 소유 자리에 넘기는
것은 안전하지만 그 반대는 아니고, 방향을 다루려면 부분 타입이
필요하다. 우리에겐 없으므로 정확히 일치를 요구한다. *)
List.length f.params = List.length g.params
&& List.for_all2 unify f.params g.params
&& unify f.ret g.ret
&& 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
| _ -> false
(* 제네릭 인스턴스화: TVar를 주어진 대입으로 바꾼다 *)
let rec subst env t =
(* 제네릭 인스턴스화: 타입 변수와 effect 변수를 동시에 바꾼다 *)
let rec subst tenv eenv t =
match resolve t with
| TVar v -> ( match List.assoc_opt v env with Some u -> u | None -> TVar v)
| TCon (n, args) -> TCon (n, List.map (subst env) args)
| TVar v -> ( match List.assoc_opt v tenv with Some u -> u | None -> TVar v)
| TCon (n, args) -> TCon (n, List.map (subst tenv eenv) args)
| TFn f ->
TFn
{
affine = f.affine;
params = List.map (subst env) f.params;
ret = subst env f.ret;
params = List.map (fun (o, t) -> (o, subst tenv eenv t)) f.params;
eff = subst_eff eenv f.eff;
ret = subst tenv eenv f.ret;
}
| u -> u
and subst_eff eenv (e : eff) : eff =
eff_resolve
(List.concat_map
(fun a ->
match a with
| A_var v -> (
match List.assoc_opt v eenv with Some s -> s | None -> [ a ])
| _ -> [ a ])
e)
+7 -4
View File
@@ -43,12 +43,15 @@ pub fn checkout(
}
// 빌린 값을 다른 빌림 자리로 넘기는 것은 허용된다 (복제가 아니다).
//
// each가 아니라 map인 이유: 결과를 버릴 방법이 언어에 없다. each는 값을
// 남기지 않는 클로저만 받으므로 Result를 삼킬 수 없고, ?는 클로저 밖으로
// 나가지 못한다. 실패를 못 본 척하려면 명시적으로 match해야 한다.
pub fn refund_all(
pay: PaymentGateway,
ids: List[OrderId],
) effects {PaymentGateway.refund} -> Result[Unit, PayError] {
List.each(ids, fn(id) {
refund_order(pay, id)?
Ok(unit)
) effects {PaymentGateway.refund} -> List[Result[Receipt, PayError]] {
List.map(ids, fn(id) {
refund_order(pay, id)
})
}
+79 -40
View File
@@ -1,26 +1,38 @@
// 05. 컴파일 에러가 나야 하는 코드
// 05. move / affinity 검사기가 거부해야 하는 코드
//
// 각 함수는 주석에 적힌 진단 하나를 정확히 내야 한다.
// 체커가 생기면 그대로 테스트 케이스가 된다.
// 09, 10과 같은 이유로 외부 타입이 하나도 없다. affinity의 뿌리는 capability라
// 자원 타입을 이 파일에서 정의해야 검사기가 affine임을 알 수 있다.
// 외부 타입은 affine임을 증명할 수 없으므로 copyable로 취급된다.
// close는 파일을 소비한다: own 유표기
pub capability File {
fn size() -> Int
}
pub capability Gateway {
fn refund(id: Int) effects {Gateway.refund}
}
pub capability Registry {
fn add(h: fn()) effects {Registry.add}
}
// 파일을 소비하는 함수: own 유표기
pub fn close(own f: File) effects {File.close}
// [E-move-after-move] affine 값의 이중 소비
pub fn double_close(own f: File) effects {File.close} {
// 빌리기만 하는 함수: 무표기
pub fn size_of(f: File) -> Int {
f.size()
}
// --- 통과해야 하는 것 ---
pub fn use_then_close(own f: File) effects {File.close} -> Int {
let n = size_of(f)
close(f)
close(f) // ERROR: f는 이미 move됨 (앞줄에서 소비)
n
}
// [E-move-join] 분기 병합은 보수적 합집합
pub fn conditional_close(own f: File, c: Bool) effects {File.close} {
if c {
close(f)
}
close(f) // ERROR: f는 이 분기에서 move됨 (조건부 소비)
}
// 정당한 형태 — 양쪽 분기에서 소비하면 통과해야 한다.
// 양쪽 분기에서 소비하면 통과한다
pub fn both_branches_close(own f: File, c: Bool) effects {File.close} {
if c {
close(f)
@@ -29,48 +41,75 @@ pub fn both_branches_close(own f: File, c: Bool) effects {File.close} {
}
}
// 빌린 값을 다른 빌림 자리로 넘기는 것은 복제가 아니다
pub fn borrow_twice(f: File) -> Int {
size_of(f) + size_of(f)
}
// affine 값을 capture한 클로저는 affine fn이다
pub fn deferred_close(own f: File) -> affine fn() effects {File.close} {
fn() { close(f) }
}
// --- 여기서부터 전부 오류다 ---
// [E-move-after-move] affine 값의 이중 소비
pub fn double_close(own f: File) effects {File.close} {
close(f)
close(f)
}
// [E-move-join] 분기 병합은 보수적 합집합
pub fn conditional_close(own f: File, c: Bool) effects {File.close} {
if c {
close(f)
}
close(f)
}
// [E-use-escape] 빌린 값의 반환
pub fn leak_capability(pay: PaymentGateway) -> PaymentGateway {
pay // ERROR: 빌린 값은 반환할 수 없음 (own이 아니다)
pub fn leak_capability(pay: Gateway) -> Gateway {
pay
}
// [E-use-escape] 빌린 값의 저장
pub struct Holder {
pay: PaymentGateway,
pay: Gateway,
}
pub fn store_capability(pay: PaymentGateway) -> Holder {
Holder { pay: pay } // ERROR: 빌린 값은 struct에 저장할 수 없음
pub fn store_capability(pay: Gateway) -> Holder {
Holder { pay: pay }
}
// [E-use-escape] 빌린 값을 capture한 클로저를 own 자리에 전달
pub fn register(own handler: fn()) effects {Registry.add}
// [E-use-escape] 빌린 값을 다른 함수에 소유로 넘긴다
pub fn give_away(f: File) effects {File.close} {
close(f)
}
pub fn escape_via_closure(pay: PaymentGateway) effects {Registry.add} {
register(fn() { pay.refund(OrderId(1)) })
// ERROR: pay를 capture한 클로저는 빌린 값이며 own 자리에 전달할 수 없음
// [E-use-escape] 빌린 값을 capture한 클로저를 own 자리에 넘긴다
pub fn register(own h: fn() effects {Gateway.refund}) effects {Registry.add}
pub fn escape_via_closure(pay: Gateway) effects {Registry.add} {
register(fn() { pay.refund(1) })
}
// [E-affinity-transitive] affine 필드를 가진 타입을 copyable로 선언
pub copyable struct Box {
f: File, // ERROR: affine 필드(File)와 copyable 선언은 공존할 수 없음
f: File,
}
// [E-callable-affinity] affine 값을 capture한 클로저를 fn 위치에 대입
// [E-callable-affinity] affine 값을 capture한 클로저를 fn 위치에 반환
pub fn misuse_affine_closure(own f: File) -> fn() effects {File.close} {
fn() { close(f) }
// ERROR: f를 capture했으므로 타입은 affine fn()이며 fn() 위치에 대입할 수 없음
}
// [E-spawn-capture] spawn 클로저 mutable capture
pub fn spawn_mutable(sc: TaskScope, mut counter: Int) effects {TaskScope.spawn} {
scope s = sc {
sc.spawn(fn() { counter = counter + 1 })
// ERROR: spawn 클로저는 mutable 참조를 capture할 수 없음
}
}
// [E-effect-undeclared] 선언되지 않은 effect
pub fn silent_write(log: Logger) {
log.write("hi") // ERROR: effect Logger.write가 시그니처에 선언되지 않음
// [E-closure-mut-capture] 클로저 mut 바인딩을 capture할 수 없다
pub fn capture_mut(own f: File, pay: Gateway)
effects {File.close, Registry.add, Gateway.refund} {
let mut counter = 0
register(fn() {
counter = counter + 1
pay.refund(counter)
})
close(f)
}
+25 -1
View File
@@ -2,7 +2,9 @@
//
// 05와 목적이 다르다. 05는 구문은 맞지만 검사기가 거부해야 하는 파일이고,
// 이 파일은 파서가 거부해야 하는 파일이다.
// 아직 오류 복구가 없으므로 파서는 첫 오류에서 멈춘다 — 한 번에 하나씩 확인한다.
// 파서는 항목 단위로 회복한다. 오류가 난 선언은 통째로 버리고 다음 선언에서
// 다시 시작하므로, 한 항목에 오류 하나가 상한이다. 이 파일은 항목마다 하나씩
// 심어 회복이 실제로 되는지 본다 — 아래 넷이 모두 보고되어야 한다.
// [E-syntax-effect-union] 파라미터 위치의 합집합은 문법에 존재하지 않는다.
// 검사기가 아니라 파서가 거부한다 (eff_param 프로덕션에 "|"가 없다).
@@ -19,3 +21,25 @@ pub fn classify(e: PayError) -> String {
_ => "other",
}
}
// [E-syntax-trailing-comma] 다중 줄 목록에는 후행 콤마가 필요하다.
// 줄바꿈이 목록을 닫으려 하면 원인을 직접 말한다.
pub fn missing_comma(
a: Int,
b: Int
) -> Int
// ERROR (parse): 다중 줄 목록에는 후행 콤마가 필요합니다
// [E-syntax-scope-parent] 자식 scope는 부모를 명시해야 한다.
// 부모 없는 작업이 생기지 않게 하는 것은 문법의 일이다.
pub fn orphan(root: TaskScope) {
scope sc {
sc.spawn(fn() { })
}
}
// ERROR (parse): = (자식 scope의 부모를 명시해야 합니다)
// 회복이 되었다는 증거: 이 마지막 선언은 정상적으로 읽혀야 한다.
pub fn fine(n: Int) -> Int {
n + 1
}
+86
View File
@@ -0,0 +1,86 @@
// 10. effect 검사기가 거부해야 하는 코드
//
// 09와 같은 이유로 외부 타입이 하나도 없다. capability를 이 파일에서 정의해야
// 메서드의 effect가 알려지고, 검사기가 실제로 판정할 수 있다.
pub capability Db {
fn read(id: Int) effects {Db.read} -> Int
fn write(id: Int, v: Int) effects {Db.write}
}
pub capability Log {
fn write(msg: String) effects {Log.write}
}
// --- 통과해야 하는 것 ---
pub fn get(db: Db, id: Int) effects {Db.read} -> Int {
db.read(id)
}
pub fn copy(db: Db, from: Int, to: Int) effects {Db.read, Db.write} {
db.write(to, db.read(from))
}
// 헬퍼를 부르면 헬퍼의 effect를 물려받는다
pub fn get_twice(db: Db, id: Int) effects {Db.read} -> Int {
get(db, id) + get(db, id)
}
// effect 변수: 결정 위치의 변수가 인자의 effect로 묶인다
pub fn twice[e: effects](f: fn() effects e) effects e {
f()
f()
}
pub fn log_twice(log: Log) effects {Log.write} {
twice(fn() { log.write("hi") })
}
// effect 없는 함수는 effects 절이 없다
pub fn pure_add(a: Int, b: Int) -> Int {
a + b
}
// --- 여기서부터 전부 오류다 ---
// [E-effect-undeclared] 선언 없이 capability 메서드를 부른다
pub fn silent_read(db: Db) -> Int {
db.read(1)
}
// [E-effect-undeclared] 일부만 선언했다
pub fn partial(db: Db, id: Int) effects {Db.read} {
db.write(id, db.read(id))
}
// [E-effect-undeclared] 헬퍼가 수행하는 effect도 물려받아야 한다
pub fn via_helper(db: Db, id: Int) -> Int {
get(db, id)
}
// [E-effect-undeclared] 클로저를 통해 새어 나오는 effect
pub fn via_closure(log: Log) {
twice(fn() { log.write("hi") })
}
// [E-effect-closure-annotated] 클로저가 선언한 것보다 많이 수행한다
pub fn closure_lies(log: Log) effects {Log.write} {
twice(fn() effects {} { log.write("hi") })
}
// [E-effect-param] 파라미터가 허용한 effect를 넘는 함수를 넘긴다
pub fn takes_pure(f: fn() effects {}) {
f()
}
pub fn pass_impure(log: Log) effects {Log.write} {
takes_pure(fn() { log.write("hi") })
}
// [E-capability-static] capability 메서드를 타입 이름으로 부른다.
// 이것이 허용되면 capability 없이 effect를 수행할 수 있게 되어
// "capability 없이는 effect를 수행할 수 없다"는 정리가 무너진다.
pub fn no_instance() effects {Db.read} -> Int {
Db.read(1)
}
+108
View File
@@ -0,0 +1,108 @@
// 11. exhaustiveness 검사기가 거부해야 하는 코드
//
// 철학 1의 대표 항목이자, interface hash가 enum 정의 본문을 입력으로 삼는 이유다.
// upstream에 variant가 하나 늘면 downstream의 match가 깨져야 하는데,
// 이 검사가 없으면 깨질 것이 없다.
pub enum Shape {
Circle(Int),
Rect(Int, Int),
Point,
}
// --- 통과해야 하는 것 ---
pub fn area(s: Shape) -> Int {
match s {
Circle(r) => r * r,
Rect(w, h) => w * h,
Point => 0,
}
}
pub fn with_wildcard(s: Shape) -> Int {
match s {
Circle(r) => r,
_ => 0,
}
}
pub fn nested_full(o: Option[Shape]) -> Int {
match o {
Some(Circle(r)) => r,
Some(Rect(w, h)) => w * h,
Some(Point) => 0,
None => 0,
}
}
pub fn results(r: Result[Int, Int]) -> Int {
match r {
Ok(n) => n,
Err(e) => e,
}
}
pub fn flags(b: Bool) -> Int {
match b {
true => 1,
false => 0,
}
}
// --- 여기서부터 전부 오류다 ---
// [E-match-missing] variant 하나가 빠졌다
pub fn missing_variant(s: Shape) -> Int {
match s {
Circle(r) => r,
Rect(w, h) => w * h,
}
}
// [E-match-missing] Bool도 생성자 집합이 유한하다
pub fn missing_false(b: Bool) -> Int {
match b {
true => 1,
}
}
// [E-match-missing] Option
pub fn missing_none(o: Option[Int]) -> Int {
match o {
Some(n) => n,
}
}
// [E-match-missing] 중첩된 자리에서 빠진 경우도 찾는다
pub fn missing_nested(o: Option[Shape]) -> Int {
match o {
Some(Circle(r)) => r,
None => 0,
}
}
// [E-match-missing] Int 리터럴은 생성자 집합이 무한하다
pub fn missing_literal(n: Int) -> Int {
match n {
0 => 1,
1 => 2,
}
}
// [E-match-unreachable] 앞의 와일드카드에 가린다
pub fn shadowed(s: Shape) -> Int {
match s {
_ => 0,
Point => 1,
}
}
// [E-match-unreachable] 같은 생성자가 두 번
pub fn duplicated(s: Shape) -> Int {
match s {
Circle(r) => r,
Circle(x) => x,
_ => 0,
}
}
+33
View File
@@ -0,0 +1,33 @@
// 12. 표준 라이브러리 시그니처가 실제로 검사된다
//
// std가 생기기 전에는 List.each가 모르는 이름이라 조용히 통과했다.
// "모르는 것을 틀렸다고 말하지 않는다"는 맞는 원칙이지만, 그 그늘에
// 검사되지 않는 영역이 숨어 있었다. 이제 그늘이 없다.
//
// 이 파일은 세 가지 오류를 낸다.
import "cool.dev/std/list" as List
import "cool.dev/std/int" as Int
pub capability Console {
fn print(s: String) effects {Console.print}
}
// (1) effect 다형성. e는 클로저의 시그니처에서 {Console.print}로 묶이고,
// 그것이 이 함수의 effects 절 {}를 넘는다. effect 변수가 호출 지점에서
// 실제로 해소된다는 증거다 — 이게 안 걸리면 규칙이 장식이다.
pub fn leaks_effect(c: Console, xs: List[Int]) {
List.each(xs, fn(n) {
c.print(Int.show(n))
})
}
// (2) 인자 개수
pub fn wrong_arity(xs: List[Int]) -> Int {
List.len(xs, 1)
}
// (3) 반환 타입
pub fn wrong_type(xs: List[Int]) -> String {
List.len(xs)
}
+35
View File
@@ -0,0 +1,35 @@
// 13. 두 lint — 미사용 import와 effect 과잉 선언
//
// 둘 다 취향 문제가 아니라 비용 문제다.
//
// 미사용 import: 쓰지도 않는 모듈의 시그니처가 바뀌면 이 모듈이 재검사된다.
// 증분 루프의 비용을 아무 이유 없이 넓히는 선언이다.
//
// effect 과잉 선언: 선언한 effect를 수행하지 않으면 호출자는 하지도 않는
// 일에 대한 의무를 진다 — 자기 effects 절을 넓히거나 capability를 받아온다.
// 시그니처는 실제보다 좁아도 안 되고 넓어도 안 된다.
import "cool.dev/std/list" as List
// ERROR: List을(를) 가져왔지만 쓰지 않습니다
pub capability Db {
fn read(id: Int) effects {Db.read} -> Int
fn write(id: Int) effects {Db.write}
}
// Db.write는 선언만 하고 수행하지 않는다.
pub fn only_reads(db: Db, id: Int) effects {Db.read, Db.write} -> Int {
db.read(id)
}
// ERROR: only_reads은(는) Db.write을(를) 선언했지만 수행하지 않습니다
// 정확히 선언하면 통과한다.
pub fn honest(db: Db, id: Int) effects {Db.read} -> Int {
db.read(id)
}
// effect 변수가 있으면 판정하지 않는다. e에 무엇이 묶일지는 호출 지점이
// 정하고, 본문만 보고는 알 수 없다 — 모르는 것을 틀렸다고 말하지 않는다.
pub fn polymorphic[e: effects](f: fn() effects e) effects e {
f()
}
+32 -7
View File
@@ -12,19 +12,44 @@
| 02_higher_order_effects | effect 변수, 구문 수준 제한, 명시적 인스턴스화 |
| 03_scope_concurrency | TaskScope, 이름 있는 scope, 중첩 시 수명 표현 |
| 04_enum_match_interface | enum 정의 본문이 interface surface에 들어가는 경로 |
| 05_move_errors | **에러가 나야 하는** 코드 — 진단 하나씩 |
| 05_move_errors | **move/affinity 검사기가** 거부해야 하는 코드 (자원을 직접 정의) |
| 06_affine_closure | callable affinity (fn vs affine fn), own과의 직교성 |
| 07_module_interface | interface artifact가 담아야 할 것 전부 |
| 08_syntax_errors | **파서가** 거부해야 하는 코드 |
| 09_type_errors | **타입 검사기가** 거부해야 하는 코드 (외부 타입 0개) |
| 10_effect_errors | **effect 검사기가** 거부해야 하는 코드 (capability를 직접 정의) |
| 11_exhaustiveness | **exhaustiveness 검사기가** 거부해야 하는 코드 |
05, 08, 09 통과하면 안 되는 파일이다. 각 함수 주석의 `[E-...]` 태그가 기대
진단이며, 의 목적이 다르다 — **08은 파서가, 09는 타입 검사기가, 05는 아직
는 move/affinity 검사가** 거부해야 한다. 단계별로 파일을 나눈 이유는
앞 단계가 첫 오류에서 멈추면 뒤 단계 케이스에 영영 도달하지 못하기 때문이다.
05, 08, 09, 10은 통과하면 안 되는 파일이다. 각 함수 주석의 `[E-...]` 태그가
기대 진단이며, 의 목적이 다르다 — **08은 파서가, 09는 타입 검사기가,
10은 effect 검사기가, 05는 move/affinity 검사가** 거부해야 한다. 단계별로
파일을 나눈 이유는 앞 단계가 첫 오류에서 멈추면 뒤 단계 케이스에 영영
도달하지 못하기 때문이다.
09에는 외부 타입이 하나도 없다. 전부 모듈 안에서 정의되므로 검사기가
TUnknown으로 빠져나갈 구석이 없다 — 검사기에 이빨이 있는지 보는 파일이다.
05, 09, 10에는 외부 타입이 하나도 없다. 전부 모듈 안에서 정의되므로 검사기가
빠져나갈 구석이 없다 — 검사기에 이빨이 있는지 보는 파일이다. 10은
capability를 직접 정의해야 메서드의 effect가 알려지고, 05는 affinity의 뿌리가
capability라 자원 타입을 정의해야 affine임이 유도된다.
13은 두 lint다. 미사용 import는 재검사 범위를 넓히고, effect 과잉 선언은
호출자에게 없는 의무를 지운다 — 둘 다 취향이 아니라 비용이다. 미사용
import는 lint이므로 뒤 단계를 막지 않는다: 같은 파일의 타입 오류가 함께
보고된다.
12는 표준 라이브러리가 생긴 뒤에야 가능해진 파일이다. std가 없을 때는
`List.each`가 모르는 이름이라 조용히 통과했다 — "모르는 것을 틀렸다고 말하지
않는다"는 맞는 원칙이지만 그 그늘에 검사되지 않는 영역이 있었다.
01~04, 06, 07은 `coolc check`를 통과한다 (exit 0).
`modules/`는 모듈 경계다. `coolc check modules/area.cool`이 import를 따라
shapes를 먼저 검사하고, `coolc iface modules/shapes.cool`이 downstream이
보는 표면과 그 해시를 보여준다.
`run/`은 실제로 돈다. `coolc run run/hello.cool`. 이 파일들은 검사를
통과한다가 아니라 무엇을 출력하는지까지 말한다 — 기대 출력이 주석에 있고
같은 것을 test/가 검사한다. main이 선언한 capability만 런타임이 넘기므로,
파라미터에서 Console을 지우면 출력할 방법이 프로그램 안에 없다.
파서는 첫 오류에서 멈춘다(오류 복구 미구현). 타입 검사기는 오류를 전부 모은다.
+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)
}
+13
View File
@@ -0,0 +1,13 @@
// 모듈 B. A의 표면에만 의존한다.
//
// Shape의 variant가 늘면 이 match가 깨진다 — 그래서 enum 정의 본문이
// interface hash 입력이고, A의 시그니처 변경은 여기까지 전파되어야 한다.
import "shapes" as Shapes
pub fn area(s: Shapes.Shape) -> Int {
match s {
Shapes.Circle(r) => Shapes.double(r),
Shapes.Square(w) => w * w,
}
}
+11
View File
@@ -0,0 +1,11 @@
// 모듈 A. downstream이 보는 것은 이 파일의 exported surface뿐이다.
pub enum Shape {
Circle(Int),
Square(Int),
}
pub fn double(n: Int) -> Int {
// 본문. 이 안을 아무리 고쳐도 interface hash는 변하지 않는다.
n + n
}
+57
View File
@@ -0,0 +1,57 @@
// 실행 의미를 한 파일에 모은 것: ?, mut, struct, match, scope.
//
// 기대 출력:
// 7
// 6
// err
// in scope
import "cool.dev/std/int" as Int
pub capability Console {
fn print(s: String) effects {Console.print}
}
pub enum E {
Bad,
}
pub copyable struct P {
x: Int,
y: Int,
}
pub fn half(n: Int) -> Result[Int, E] {
if n % 2 == 0 {
Ok(n / 2)
} else {
Err(Bad)
}
}
pub fn twice(n: Int) -> Result[Int, E] {
let a = half(n)?
let b = half(a)?
Ok(a + b)
}
pub fn show_res(r: Result[Int, E]) -> String {
match r {
Ok(v) => Int.show(v),
Err(_) => "err",
}
}
pub fn main(c: Console, root: TaskScope) effects {Console.print} {
let mut total = 0
let p = P { x: 3, y: 4 }
total = total + p.x + p.y
c.print(Int.show(total))
c.print(show_res(twice(8)))
c.print(show_res(twice(7)))
scope sc = root {
sc.spawn(fn() {
c.print("in scope")
})
}
}
+40
View File
@@ -0,0 +1,40 @@
// 실행되는 첫 프로그램.
//
// main은 자기가 선언한 capability만 받는다. Console을 파라미터에서 지우면
// print할 방법이 프로그램 안에 없다 — ambient authority가 없다는 것의
// 실행 시점 의미다.
//
// 표준 라이브러리도 명시적으로 가져온다. prelude가 없다 —
// 암묵적으로 끌어오지 않는다는 규칙에 예외를 두지 않는다.
//
// 기대 출력:
// area = 12
// area = 9
// area = 3
import "cool.dev/std/list" as List
import "cool.dev/std/string" as String
import "cool.dev/std/int" as Int
pub capability Console {
fn print(s: String) effects {Console.print}
}
pub enum Shape {
Circle(Int),
Square(Int),
}
pub fn area(s: Shape) -> Int {
match s {
Circle(r) => r * r * 3,
Square(w) => w * w,
}
}
pub fn main(c: Console) effects {Console.print} {
let shapes = [Circle(2), Square(3), Circle(1)]
List.each(shapes, fn(s) {
c.print(String.concat("area = ", Int.show(area(s))))
})
}
+3
View File
@@ -0,0 +1,3 @@
// 표준 라이브러리: 불리언.
pub fn show(b: Bool) -> String
+7
View File
@@ -0,0 +1,7 @@
// 표준 라이브러리: 정수.
pub fn show(n: Int) -> String
pub fn abs(n: Int) -> Int
// 실패할 수 있으므로 Result다. 예외도, 0을 돌려주는 관례도 없다.
pub fn parse(s: String) -> Result[Int, String]
+59
View File
@@ -0,0 +1,59 @@
// 표준 라이브러리: 리스트.
//
// 본문이 없다. 런타임이 구현하고, 이 파일은 그 계약을 말한다.
// 그래서 이 파일은 구현이 아니라 시험대다 — effect 다형성이 실제로 쓸 만한지가
// each, map, fold, filter에서 결정된다. 규칙이 틀렸으면 여기서 드러난다.
//
// e는 파라미터의 effect 슬롯에 홀로 나타난다 (결정 위치). 호출 지점에서
// 인자의 시그니처를 읽어 묶인다 — 추론이 아니라 읽기다.
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]
// 번호를 붙인 값. 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](
xs: List[a],
f: fn(a) effects e,
) effects e
pub fn map[a, b, e: effects](
xs: List[a],
f: fn(a) effects e -> 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
+18
View File
@@ -0,0 +1,18 @@
// 표준 라이브러리: 문자열.
//
// 인덱싱 연산자가 언어에 없다. 문자열을 자르는 일은 이름 있는 함수가 한다 —
// 한 개념 한 방식.
pub fn len(s: String) -> Int
pub fn is_empty(s: String) -> Bool
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 -1
View File
@@ -2,4 +2,10 @@
(name test_coollang)
(libraries coollang)
(deps
(glob_files %{workspace_root}/samples/*.cool)))
(glob_files %{workspace_root}/samples/*.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)))
+1172 -7
View File
File diff suppressed because it is too large Load Diff
+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