Compare commits

...
18 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
43 changed files with 4490 additions and 179 deletions
+22
View File
@@ -60,6 +60,28 @@ m100.cool:15:3: match가 모든 경우를 덮지 않습니다 (빠진 경우: Tr
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이 필요하다.
+38 -5
View File
@@ -3,8 +3,9 @@ let usage =
사용법:
coolc check <file.cool>... 타입/effect/capability 검사 (import를 따라 모듈 그래프 전체)
coolc test <file.cool> [필터] 모듈 그래프의 test 블록 실행
coolc iface <file.cool> interface 표면과 해시 출력
coolc run <file.cool> typed IR 인터프리터로 실행
coolc run <file.cool> [인자...] typed IR 인터프리터로 실행
coolc tokens <file.cool> 토큰 덤프 (렉서 디버깅)
coolc ast <file.cool> 구문 트리 덤프 (파서 디버깅)
coolc deps <file.cool> 외부 참조 목록 (모듈의 의존 표면)
@@ -76,13 +77,45 @@ let () =
match List.tl argv with
| "check" :: files -> check_graph files
| [ "iface"; file ] -> dump_iface file
| [ "run"; file ] -> (
| "test" :: file :: rest -> (
let filter = match rest with f :: _ -> f | [] -> "" in
let st = Coollang.Session.create ~root:(Filename.dirname file) () in
match Coollang.Session.run st file with
| Ok out ->
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
| Error e ->
| out, Some e ->
(* 실패해도 그때까지의 출력을 먼저 보여준다 *)
print_string out;
flush stdout;
prerr_endline (Coollang.Session.string_of_error e);
1)
| [ "tokens"; file ] -> dump_tokens file
+383
View File
@@ -0,0 +1,383 @@
# 개밥 먹기 보고 — samples/app을 쓰면서 걸린 것들
2026-08-30. coollang으로 처음 쓴 "일하는 프로그램" 하나(설정 파서 + 리포트
도구, 2모듈 292줄)에서 실제로 걸린 마찰을 적는다.
v0의 샘플 16개는 전부 검사기를 시험하려고 쓴 것이고, 그래서 "언어가 쓸
만한가"에 대해서는 아무것도 말해주지 않았다. 이 문서가 v0가 남기는 마지막
데이터이자 v1 설계의 첫 입력이다.
기록 원칙: **불편은 증거와 함께 적고, 해법은 제안까지만 한다.** 여기서
바로 고치면 그것은 개밥 먹기가 아니라 기능 추가가 된다.
---
## 현황 한눈에
| | 항목 | 상태 |
|---|---|---|
| F1 | 리스트의 n번째를 꺼낼 방법이 없다 | **해결**`List.first`, `List.nth` |
| F2 | ~~`else if`가 없다~~ | **취소** — 관찰자가 틀렸다 |
| F3 | `String.concat`이 2항이라 중첩 지옥 | **해결**`String.join` |
| F4 | struct의 한 필드만 바꿀 방법이 없다 | **닫음 — 넣지 않는다** |
| F5 | fold에 인덱스가 없다 | **해결**`List.enumerate` |
| F6 | Option/Result에 조작 함수가 없다 | **해결**`std/option`, `std/result` |
| F7 | std와 인터프리터가 어긋날 수 있다 | **해결** — 양방향 테스트 |
| F8 | 미사용 지역 변수·파라미터를 안 잡는다 | **닫음 — 넣지 않는다** |
여덟 항목이 전부 처리됐다. 다섯은 해결, 하나는 취소(관찰자 오류), 둘은
"넣지 않는다"로 닫았다.
**닫은 둘을 "보류"가 아니라 "닫음"으로 적는 이유**: 근거까지 적어두지
않으면 다음에 같은 논의를 처음부터 다시 하게 된다. 마음이 바뀔 조건도
같이 적었다 — 그 조건이 오면 다시 연다.
## 잘 된 것부터
**1. 시그니처가 프로그램의 전부를 말한다.**
```cool
pub fn main(c: Console, f: File, a: Args)
effects {Console.print, File.read, Args.all}
```
이 한 줄을 읽으면 이 프로그램이 할 수 있는 일이 끝난다. 네트워크를 쓸 수
없고, 다른 파일을 쓸 수 없고, 프로세스를 띄울 수 없다 — 문서가 아니라
컴파일러가 보장한다. 300줄을 쓰는 내내 이것이 어색하지 않았다. 오히려
`f.read`를 쓰려고 `File`을 인자에 추가하는 순간이 "이 함수가 권한을 하나
더 갖는다"는 사실을 자각하게 만들었다.
**2. `?`가 기대대로 동작했다.**
```cool
let path = first_arg(a.all())?
let text = f.read(path)?
```
읽기 좋고, 실패 경로가 보이고, 삼켜지지 않는다.
**3. 소진성이 실전에서 값을 했다.**
`Value`에 경우 하나(`List`)를 추가해 봤다:
```
config.cool:54:5: 빠진 경우: List(_)
config.cool:62:5: 빠진 경우: List(_)
config.cool:196:5: 빠진 경우: List(_)
config.cool:204:5: 빠진 경우: List(_)
```
고쳐야 할 자리 넷을 전부, 정확히 짚었다. 이것이 없으면 `show_value`
고치고 `get_int`를 잊는다.
**4. 모듈 경계가 자연스러웠다.** 파싱(`config`)과 출력(`main`)을 나누는 데
마찰이 없었고, `Cfg.Config` 같은 한정 이름이 오히려 읽기 좋았다.
---
## 걸린 것 — 심각한 순서로
### F1. 리스트의 n번째를 꺼낼 방법이 없다 (심각)
인덱싱 연산자를 뺀 결정 자체는 옳다고 본다. 그런데 `std`에 대안이 없다.
"첫 원소"를 꺼내려고 이 코드를 네 번 썼다:
```cool
pub fn first_text(xs: List[String]) -> Option[String] {
List.fold(xs, None, fn(acc, x) {
match acc {
Some(prev) => Some(prev),
None => Some(x),
}
})
}
```
`String.split(line, "=")`의 결과에서 앞의 둘을 꺼내는 데는 보조 struct
`Pick`까지 만들어야 했다 — 순전히 "몇 번째를 보고 있는가"를 나르려고.
**304줄 중 약 40줄이 이 문제 하나 때문에 존재한다.**
> 제안: `List.first`, `List.nth(xs, i) -> Option[a]`, 그리고 `String.split`
> 같은 자리에서 흔한 `List.split_first(xs) -> Option[(a, List[a])]`.
> 튜플이 없으므로 마지막 것은 문법 결정을 동반한다.
### F2. ~~`else if`가 없다~~ — 취소. 관찰자가 틀렸다
처음 이 문서를 쓸 때 "`else if`가 없어서 3~4단 중첩이 된다"고 적었다.
**틀렸다.** `else if`는 처음부터 된다 (`lib/parser.ml:480`이 명시적으로
`else` 뒤의 `if`를 처리한다).
그런데도 `config.cool`을 중첩 `if`로 썼다. 언어가 강제한 것이 아니라
내가 확인하지 않고 습관대로 쓴 것이고, 그다음 언어를 탓했다.
고쳐 쓰니 208줄이 196줄이 됐고 `parse_line`은 4단에서 평평한 5갈래가
됐다:
```cool
if String.is_empty(line) {
cfg
} else if String.starts_with(line, "#") {
cfg
} else if List.len(parts) != 2 {
add_problem(cfg, no, ...)
} else if String.is_empty(String.trim(head_or(parts, ""))) {
add_problem(cfg, no, "이름이 비어 있습니다")
} else {
add_entry(cfg, ...)
}
```
**개밥 먹기 자체에 대한 교훈이다.** 한 사람이 쓴 300줄에서 나온 불편은
언어의 성질일 수도 있고 그 사람의 습관일 수도 있다. 둘을 나누려면 불편을
적을 때마다 "언어가 정말 막는가"를 확인해야 한다. 확인 없이 적은 것 하나가
"심각" 등급을 달고 v1 설계 입력이 될 뻔했다.
나머지 항목들은 확인했다 — F1은 `List.first`/`nth`가 std에 실제로 없고,
F3은 `String.join`이 실제로 없다.
### F3. `String.concat`이 2항이라 중첩 지옥이 된다 (심각)
리포트 한 줄이 이렇게 생겼다:
```cool
c.print(String.concat(" ", String.concat(e.key,
String.concat(" = ", String.concat(Cfg.show_value(e.value),
String.concat(" (", String.concat(Cfg.type_name(e.value), ")")))))))
```
읽을 수 없다. 이 프로그램에서 가장 나쁜 코드이고, 원인은 명확하다.
> 제안 두 가지. (a) `String.join(sep, List[String])` — 작고 안전하다.
> (b) 문자열 보간 `"${key} = ${value}"` — 훨씬 낫지만 문법과 타입 규칙을
> 정해야 하고, "한 개념 한 방식"에서 concat과 겹친다.
> 지금 판단으로는 (a)를 먼저 넣고 (b)는 체감 데이터를 더 모은 뒤.
### F4. struct의 한 필드만 바꿀 방법이 없다 — 닫음. 넣지 않는다
```cool
pub fn add_entry(cfg: Config, e: Entry) -> Config {
Config {
entries: List.push(cfg.entries, e),
problems: cfg.problems, // ← 안 바뀌는데 적어야 한다
}
}
```
필드가 둘이라 견딜 만하지만 다섯이면 못 쓴다. `take_at`은 세 필드를 매번
전부 나열한다.
> 처음 제안: `Config { ..cfg, entries: x }`. affine 타입에서는 `cfg`가
> move되는 것이므로 소유권 규칙과 충돌하지 않는다고 적었다.
**결론: 넣지 않는다.** 처음 적을 때 놓친 것이 있다.
전체 나열이 귀찮은 것은 맞다. 그런데 **그 귀찮음이 값을 한다.** `Config`
필드를 하나 추가하면 지금은 모든 생성 지점이 "필드가 빠졌다"고 컴파일
오류를 낸다. 컴파일러가 전부 방문하도록 강제한다.
`..cfg`를 넣으면 그것이 사라진다. 새 필드가 조용히 base의 값을 이어받고,
정말로 손봐야 했던 자리를 지나친다.
**"오류를 더 빨리 잡는다"가 1번 목표인데 F4는 정확히 그것을 깎는 거래다.**
브레비티를 얻고 강제 방문을 잃는다. 그 거래가 옳다는 근거가 지금 없다.
소유권 규칙과 충돌하지 않는다는 처음의 관찰은 맞지만, 충돌하지 않는 것과
넣을 값이 있는 것은 다른 문제다.
> 다시 열 조건: 다음 개밥 먹기에서 필드 5개 이상인 struct의 갱신 함수가
> 여러 개 나오고, **매번의 전체 나열이 실제로 아무것도 잡지 못했다면**
> 그때 넣는다. 반대로 한 번이라도 "필드가 빠졌다"가 진짜 버그를 잡았다면
> 이 항목은 영구히 닫힌다.
### F5. fold에 인덱스가 없다 — 해결 (`List.enumerate`)
줄 번호를 세려고 struct를 하나 더 만들었다:
```cool
pub copyable struct Numbered {
no: Int,
cfg: Config,
}
```
"인덱스가 필요한 fold"는 드문 요구가 아니다.
> 처음 제안: `List.fold_indexed`. `List.enumerate`가 더 조합적이지만
> 튜플이 없어서 불가능하다고 적었다.
**"튜플이 없어서 불가능하다"가 틀렸다.** 제네릭 struct 하나면 된다:
```cool
pub copyable struct Indexed[a] {
i: Int,
value: a,
}
pub fn enumerate[a](xs: List[a]) -> List[Indexed[a]]
```
`fold_indexed`보다 이쪽이 낫다. `enumerate` 하나가 기존 `each`/`map`/
`filter`/`fold` 전부와 조합된다. `fold_indexed`를 만들면 `map_indexed`,
`each_indexed`가 따라오고 그것이 "한 개념 한 방식"을 깨는 방향이다.
`samples/app`에서 `Numbered` struct가 사라졌다 (244줄 → 232줄):
```cool
// 전 — 줄 번호를 나르려고 struct를 하나 더 만들었다
let start = Numbered { no: 1, cfg: Config { entries: [], problems: [] } }
List.fold(lines, start, fn(acc, line) {
Numbered { no: acc.no + 1, cfg: parse_line(acc.cfg, acc.no, line) }
}).cfg
// 후
let lines = List.enumerate(String.split(text, "\n"))
List.fold(lines, empty, fn(cfg, l) { parse_line(cfg, l.i + 1, l.value) })
```
이것이 **관찰자가 틀린 두 번째 사례**다(F2에 이어). 둘 다 "언어가 못
한다"고 적었는데 확인해 보니 됐다. 마찰을 적을 때 "정말 막히는가"를
확인하는 절차가 없으면 이런 것이 v1 설계 입력으로 들어간다.
### F6. Option/Result에 조작 함수가 하나도 없다 (중간)
`map`, `unwrap_or`, `or_else`가 없어서 전부 `match`로 풀었다. `match`
나쁜 것은 아니지만, 세 줄이면 될 것이 여섯 줄이 된다.
> 제안: `Option.map/unwrap_or`, `Result.map/map_err/unwrap_or`.
> 타입 이름이 함수의 이름공간이라는 규칙이 이미 있으므로 문법 결정은 없다.
### F7. std를 늘릴 때마다 인터프리터를 고쳐야 한다 (구조)
`std/list.cool``filter`를 적으면 `lib/interp.ml`에도 구현을 넣어야
한다. 두 곳이 어긋나면 검사는 통과하고 실행이 죽는다.
이것은 v0의 구조적 한계이고 v1에서 사라진다(std를 coollang으로 구현).
다만 v0 동안은 **std 시그니처와 런타임 구현이 일치하는지 검사하는 테스트**가
있어야 한다. 지금은 없다.
---
### F8. 미사용 지역 변수와 파라미터를 안 잡는다 — 닫음. 넣지 않는다
투어용 예제를 쓰다 발견했다. 이 코드가 아무 말 없이 통과한다:
```cool
pub fn f(used: Int, never_used: Int) -> Int {
let alive = used + 1
let dead = 999
alive
}
```
**미사용 import는 오류로 막아놓고 미사용 지역 변수는 통과시킨다.** 둘 다
"쓰지 않는 선언"인데 한쪽만 잡으니 규칙이 고르지 않아 보인다.
다만 근거의 성격은 다르다. import를 막은 이유는 재검사 범위를 넓히기
때문이었고 — 그건 이 아키텍처의 실제 비용이다 — 미사용 지역 변수에는
그런 비용이 없다. 그냥 죽은 코드다.
그래서 이것은 "고치면 되는 항목"이 아니라 **판단이 필요한 항목**이다.
철학 1(오류를 더 빨리 잡는다)에는 부합하지만, 디버깅 중에 한 줄 주석
처리했다고 컴파일이 막히는 것은 실제로 성가시다. 그 성가심이 잡아주는
버그보다 큰지는 지금 데이터로 알 수 없다.
**결론: 넣지 않는다.** 그리고 처음에 질문을 잘못 잡았다.
진짜 질문은 "미사용 지역 변수를 잡을 것인가"가 아니라 **"coollang에 경고
등급을 만들 것인가"**다.
지금 이 언어에는 경고가 없다. 모든 진단이 오류이고 빌드를 멈춘다. 미사용
지역 변수를 그 등급에 넣으면 디버깅 중에 한 줄 주석 처리했다고 컴파일이
막힌다 — 실제로 성가시다. 그래서 자연스럽게 "경고로 만들자"가 나오는데,
그것이 **경고 등급의 첫 입주자**가 된다. 경고 등급은 한 번 생기면 자란다.
무시되는 진단이 쌓이는 언어가 된다.
**경고가 없다는 것은 지금 이 언어의 좋은 성질이고, 죽은 지역 변수 하나
때문에 팔 것이 아니다.**
미사용 import와의 불일치는 감수한다. 근거가 다르다 — import는 재검사
범위라는 이 아키텍처의 실제 비용을 만들고, 죽은 지역 변수는 아무 비용도
만들지 않는다. 규칙이 고르지 않아 보이는 것과 근거가 없는 것은 다르다.
> 다시 열 조건: 경고 등급을 다른 이유로 만들게 되는 날. 그때는 이 항목이
> 첫 입주자가 아니라 두 번째가 되므로 비용 계산이 달라진다.
---
## 후속 (같은 날)
F1, F3, F6을 std 보강으로 처리하고 `samples/app`을 다시 썼다. 문법은
건드리지 않았다 — 표본이 한 사람이 쓴 300줄 하나뿐인데 되돌리기 비싼 축을
움직일 수는 없다.
추가한 것: `List.first`, `List.nth`, `String.join`, 그리고 `std/option.cool`,
`std/result.cool` (`map`, `unwrap_or`, `ok_or`, `map_err`, `is_ok`).
결과:
| | 전 | 후 |
|---|---|---|
| config.cool | 196줄 | **148줄** |
| main.cool | 96줄 | 96줄 |
| 합계 | 292줄 | **244줄** |
출력은 한 글자도 다르지 않다.
**F1의 값이 확인됐다.** 48줄이 사라졌고 전부 config.cool에서 나왔다 —
`head_or`, `second_or`, `Pick`, `take_at`, `first_text`, `first_entry`
통째로 없어졌다. 예측(40줄쯤)과 실제(48줄)가 맞았다.
```cool
// 전
let key = String.trim(head_or(parts, "")) // + Pick struct + take_at 20줄
// 후
let key = String.trim(Option.unwrap_or(List.nth(parts, 0), ""))
```
**F3은 줄 수로는 값이 안 보인다.** main.cool이 96줄 그대로다. 4단 중첩
`String.concat`을 4줄짜리 `String.join` 배열로 바꿨으니 줄 수가 같다.
그런데 읽기는 확실히 낫다:
```cool
// 전
c.print(String.concat(" ", String.concat(e.key,
String.concat(" = ", String.concat(Cfg.show_value(e.value),
String.concat(" (", String.concat(Cfg.type_name(e.value), ")")))))))
// 후
c.print(String.join("", [
" ", e.key, " = ", Cfg.show_value(e.value),
" (", Cfg.type_name(e.value), ")",
]))
```
**줄 수는 읽기 좋음의 대리 지표일 뿐이고, F3에서 그 대리가 깨진다.**
다음 개밥 먹기에서는 줄 수 말고 다른 것을 재야 한다.
**F7도 처리했다.** `Interp.implemented` 목록과 `std/*.cool`의 선언이
서로를 덮는지 테스트가 양방향으로 검사한다. 어긋나면 빌드가 깨진다.
남은 것 없음. F5는 `List.enumerate`로 해결했고(244줄 → 232줄), F4와 F8은
"넣지 않는다"로 닫았다. 둘 다 다시 열 조건을 함께 적었다.
---
## 결론
(아래는 처음 쓸 때의 결론이다. 후속에서 F1·F3·F5·F6·F7이 해결되고 F4·F8이
닫혔으므로 지금은 역사적 기록이다.)
v1로 넘길 때 **F1과 F3이 먼저다.** 둘 다 "언어가 틀렸다"가 아니라
"std에 없어서 우회했다"이고, 우회 비용이 코드에 그대로 보인다 — 292줄 중
40줄쯤이 F1 하나 때문에 존재한다.
F2는 취소됐다. 그리고 그것이 이 문서에서 두 번째로 중요한 발견이다:
확인 없이 적은 불편 하나가 "심각" 등급을 달고 v1 설계 입력이 될 뻔했다.
반대로 **되돌리기 비싼 결정들은 하나도 후회되지 않았다.** capability를
인자로 나르는 것, effect를 시그니처에 적는 것, 실패를 버릴 수 없는 것,
소진적 match — 300줄을 쓰는 동안 이 넷이 거추장스러웠던 순간이 없었고,
소진성은 오히려 실수를 잡아줬다.
v0의 질문은 "되돌리기 비싼 결정이 옳은가"였다. 답은 **그렇다**이고,
남은 불편은 전부 되돌리기 싼 것들이다.
+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 ) *)
+162
View File
@@ -28,6 +28,16 @@ AI의 코드 생성 속도 >> 신뢰 확보 속도.
→ "Effects: +payment.refund" 수준으로 리뷰 압축
5. 한 개념 = 한 방식: syntax variant 최소화, 공식 formatter 하나,
공식 toolchain 하나. 단순성의 기준은 "작성자"가 아니라 "리뷰어와 검증기"
6. 이름은 관례가 아니라 뜻에서 고른다 — 낯섦은 한 번 치르고 끝나지만
부정확함은 읽는 사람마다 매번 치른다
→ 판정: 그 단어로 그것이 무엇인지 평범한 문장을 써 보라. 단어가 문장을
도우면 맞는 이름, 문장과 싸우면 틀린 이름이다
"크래시는 복구하는 것이 아니라 조사하는 것이다" — 돕는다
"패닉은 복구할 수 없다" — 다른 언어에서는 할 수 있어 싸운다
→ 널리 쓰인다는 것은 옳다는 증거가 아니다. 운영체제의 "fault"가 그렇다 —
잘못이 아닌 것을 잘못이라 부르고, 그 오류까지 함께 전파된다
※ 관례가 정확하면 관례를 따른다. 관례라서가 아니라 정확해서다
※ 이름에만 적용된다. 동작을 관례와 다르게 만들 이유는 아니다
■ 파생 결정
@@ -338,6 +348,111 @@ 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: 쓰지 않는 모듈의 시그니처가 바뀌면 이 모듈이 재검사된다.
증분 루프의 비용을 이유 없이 넓히는 선언은 남겨둘 수 없다.
@@ -366,6 +481,53 @@ prelude는 없다. std도 명시적으로 가져온다 — 암묵적으로 끌
것이고, 이것이 std를 "부채 상환"이 아니라 "검증"으로 본 이유다.
결과를 버릴 방법이 언어에 없다는 성질도 여기서 처음 확인됐다.
■ 개밥 먹기 2 — dogfoods/ (2026-08)
프로그램을 쓰되 돌리지 않는다. std를 선언만 두고 그것에 대고 실제 유스케이스를
쓴다. std가 원래 선언-전용이므로 실행만 빼고 전부 진짜로 검사된다 — 종이
스케치가 아니라 컴파일러가 검증한 설계다.
합격 기준이 둘이다: check exit 0 + 모듈이 실제로 해소될 것. 후자가 없으면
전자가 공허하다 — import가 조용히 해소되지 않으면 이름이 전부 불투명해져
검사기가 무엇이든 통과시킨다. 첫 시도에서 실제로 그 일이 일어났다.
2번(Atomic File Updater) 결과: ?를 자원과 함께 쓸 수 없다. 조기 반환이 정리를
건너뛰는데 검사기는 통과시킨다(v0 정책: 오용 금지, 누수 허용). 제대로 정리하면
18줄이 58줄이 되고 5단 중첩 match가 된다. resource/with가 필요한 이유가 여기
숫자로 있다.
6번(DB Pool)까지 두 점을 모아 resource/with를 넣기로 결정했다. 근거:
자원을 들면 ?를 한 번도 못 쓴다(2/2), 같은 정리 패턴을 각자 발명했다(2/2),
누수가 조용히 통과한다(2/2), 2.3~3.2배.
고통은 자원 개수가 아니라 정리의 균일성에 비례한다 — 파일은 단계마다 정리가
달라 4단 중첩이 되고, 풀은 언제나 반납이라 1단으로 접힌다.
순서가 정해졌다: 먼저 수신자를 소비하는 메서드(자원이 자기 정리를 스스로
선언할 수 있어야 한다), 그다음 resource/with. release(pool, own lease)처럼
정리에 다른 값이 필요하면 with가 자동으로 부를 수 없기 때문이다.
고침: 함수 타입과 클로저 파라미터의 own. 그 구멍이 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
+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]
}
+29 -6
View File
@@ -21,7 +21,8 @@ type ty =
}
| T_fn of {
affine : bool;
params : ty list;
(* 파라미터마다 소유권 표시. 무표기는 빌림 *)
params : fn_param_ty list;
eff : eff_atom option;
ret : ty option;
pos : pos;
@@ -29,6 +30,7 @@ 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 =
@@ -74,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 }
@@ -148,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 }
@@ -189,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 -> ()
@@ -244,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";
@@ -265,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 ':';
@@ -468,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
+652
View File
@@ -0,0 +1,652 @@
(* EBNF 읽기.
docs/grammar.ebnf를 데이터로 읽어들인다. 여기서부터 문법은 사람이 읽는
문서가 아니라 기계가 소비하는 소스가 된다.
이 파일이 존재하는 이유: 설명서와 파서가 따로 있으면 어긋난다. 실제로
어긋났고, 어긋난 줄 아무도 몰랐다. 문법을 읽을 수 있게 되면 파서와
기계적으로 대조할 수 있고, 나아가 파서를 여기서 뽑아낼 수 있다.
표기는 grammar.ebnf 머리에 적힌 그대로다:
= 정의 | 선택 [ ] 선택적 { } 반복
( ) 묶음 " " 단말 (* *) 주석 - 제외 *)
type expr =
| Ref of string (* 다른 프로덕션 또는 토큰 이름 *)
| RefArg of string * string (* 매개변수 프로덕션 참조: primary<ok> *)
| Term of string (* "fn" 같은 리터럴 단말 *)
| Seq of expr list
| Alt of expr list
| Opt of expr
| Rep of expr
| Except of expr * expr (* char - '"' *)
(* 매개변수 프로덕션. expr_ns를 표현하려면 필요하다 — if/match의 머리에서만
struct 리터럴이 금지되는데, 그 제약은 식 문법 전체를 타고 내려간다.
매개변수가 없으면 여덟 개 프로덕션을 통째로 복제해야 하고, 그러면 사람이
읽는 문서로서의 값이 사라진다. *)
type rule = { name : string; params : string list; body : expr; line : int }
type t = rule list
type error = { line : int; msg : string }
exception Error of error
(* ------------------------------------------------------------------ *)
(* 어휘 *)
(* ------------------------------------------------------------------ *)
type tok =
| T_ident of string
| T_str of string
| T_eq
| T_semi
| T_comma
| T_bar
| T_lbracket
| T_rbracket
| T_lbrace
| T_rbrace
| T_lparen
| T_rparen
| T_minus
| T_lt
| T_gt
| T_eof
let tokenize (src : string) : (tok * int) array =
let n = String.length src in
let out = ref [] in
let line = ref 1 in
let i = ref 0 in
let emit t = out := (t, !line) :: !out in
while !i < n do
let c = src.[!i] in
if c = '\n' then (
incr line;
incr i)
else if c = ' ' || c = '\t' || c = '\r' then incr i
else if c = '(' && !i + 1 < n && src.[!i + 1] = '*' then begin
(* 주석. 중첩을 허용한다 — 문법 파일에 설명이 길게 들어간다 *)
let depth = ref 0 in
let fin = ref false in
while (not !fin) && !i < n do
if !i + 1 < n && src.[!i] = '(' && src.[!i + 1] = '*' then (
incr depth;
i := !i + 2)
else if !i + 1 < n && src.[!i] = '*' && src.[!i + 1] = ')' then (
decr depth;
i := !i + 2;
if !depth = 0 then fin := true)
else (
if src.[!i] = '\n' then incr line;
incr i)
done;
if not !fin then raise (Error { line = !line; msg = "주석이 닫히지 않았습니다" })
end
else if c = '"' || c = '\'' then begin
let quote = c in
let start = !i + 1 in
incr i;
while !i < n && src.[!i] <> quote do
if src.[!i] = '\n' then
raise (Error { line = !line; msg = "단말이 닫히지 않았습니다" });
incr i
done;
if !i >= n then raise (Error { line = !line; msg = "단말이 닫히지 않았습니다" });
emit (T_str (String.sub src start (!i - start)));
incr i
end
else if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c = '_' then begin
let start = !i in
while
!i < n
&&
let d = src.[!i] in
(d >= 'a' && d <= 'z')
|| (d >= 'A' && d <= 'Z')
|| (d >= '0' && d <= '9')
|| d = '_'
do
incr i
done;
emit (T_ident (String.sub src start (!i - start)))
end
else begin
let single t =
emit t;
incr i
in
match c with
| '=' -> single T_eq
| ';' -> single T_semi
| ',' -> single T_comma
| '|' -> single T_bar
| '[' -> single T_lbracket
| ']' -> single T_rbracket
| '{' -> single T_lbrace
| '}' -> single T_rbrace
| '(' -> single T_lparen
| ')' -> single T_rparen
| '-' -> single T_minus
| '<' -> single T_lt
| '>' -> single T_gt
| _ ->
raise (Error { line = !line; msg = Printf.sprintf "알 수 없는 문자 %c" c })
end
done;
emit T_eof;
Array.of_list (List.rev !out)
(* ------------------------------------------------------------------ *)
(* 구문 *)
(* ------------------------------------------------------------------ *)
type state = { toks : (tok * int) array; mutable p : int }
let cur st = fst st.toks.(st.p)
let line st = snd st.toks.(st.p)
let adv st = if st.p < Array.length st.toks - 1 then st.p <- st.p + 1
let fail st msg = raise (Error { line = line st; msg })
let eat st t what =
if cur st = t then adv st else fail st (Printf.sprintf "%s이(가) 필요합니다" what)
(* alt := seq { "|" seq } *)
let rec parse_alt st =
let first = parse_seq st in
if cur st <> T_bar then first
else begin
let acc = ref [ first ] in
while cur st = T_bar do
adv st;
acc := parse_seq st :: !acc
done;
Alt (List.rev !acc)
end
(* seq := factor { "," factor } *)
and parse_seq st =
let first = parse_factor st in
if cur st <> T_comma then first
else begin
let acc = ref [ first ] in
while cur st = T_comma do
adv st;
acc := parse_factor st :: !acc
done;
Seq (List.rev !acc)
end
(* factor := primary { "-" primary } — 제외는 여러 번 올 수 있다 *)
and parse_factor st =
let a = ref (parse_primary st) in
while cur st = T_minus do
adv st;
a := Except (!a, parse_primary st)
done;
!a
and parse_primary st =
match cur st with
| T_ident n ->
adv st;
if cur st = T_lt then begin
adv st;
let a =
match cur st with
| T_ident a ->
adv st;
a
| _ -> fail st "매개변수 이름"
in
eat st T_gt ">";
RefArg (n, a)
end
else Ref n
| T_str s ->
adv st;
Term s
| T_lbracket ->
adv st;
let e = parse_alt st in
eat st T_rbracket "]";
Opt e
| T_lbrace ->
adv st;
let e = parse_alt st in
eat st T_rbrace "}";
Rep e
| T_lparen ->
adv st;
let e = parse_alt st in
eat st T_rparen ")";
e
| _ -> fail st "이름, 단말, 또는 묶음"
let parse (src : string) : t =
let st = { toks = tokenize src; p = 0 } in
let rules = ref [] in
while cur st <> T_eof do
let ln = line st in
let name =
match cur st with
| T_ident n ->
adv st;
n
| _ -> fail st "프로덕션 이름"
in
let params =
if cur st = T_lt then begin
adv st;
let acc = ref [] in
let rec loop () =
(match cur st with
| T_ident p ->
adv st;
acc := p :: !acc
| _ -> fail st "매개변수 이름");
if cur st = T_comma then (
adv st;
loop ())
in
loop ();
eat st T_gt ">";
List.rev !acc
end
else []
in
eat st T_eq "=";
let body = parse_alt st in
eat st T_semi ";";
rules := { name; params; body; line = ln } :: !rules
done;
List.rev !rules
let parse_result src =
match parse src with r -> Ok r | exception Error e -> Error e
(* ------------------------------------------------------------------ *)
(* 조회 *)
(* ------------------------------------------------------------------ *)
let find (g : t) name = List.find_opt (fun r -> r.name = name) g
(* 정의되지 않은 채 참조된 이름. 토큰 이름일 수도 있고 빠뜨린 프로덕션일
수도 있으므로 판정하지 않고 목록만 준다. *)
let undefined (g : t) : string list =
let defined = List.map (fun r -> r.name) g in
let seen = Hashtbl.create 32 in
let rec walk = function
| Ref n -> if not (List.mem n defined) then Hashtbl.replace seen n ()
| RefArg (n, _) ->
if not (List.mem n defined) then Hashtbl.replace seen n ()
| Term _ -> ()
| Seq xs | Alt xs -> List.iter walk xs
| Opt e | Rep e -> walk e
| Except (a, b) ->
walk a;
walk b
in
List.iter (fun r -> walk r.body) g;
Hashtbl.fold (fun k () acc -> k :: acc) seen [] |> List.sort compare
(* 어디서도 참조되지 않는 프로덕션. 시작 기호는 제외한다. *)
let unreachable (g : t) ~(start : string) : string list =
let used = Hashtbl.create 32 in
let rec walk = function
| Ref n -> Hashtbl.replace used n ()
(* 인자로 넘어간 이름도 쓰인 것이다: brace_list<field>의 field *)
| RefArg (n, a) ->
Hashtbl.replace used n ();
Hashtbl.replace used a ()
| Term _ -> ()
| Seq xs | Alt xs -> List.iter walk xs
| Opt e | Rep e -> walk e
| Except (a, b) ->
walk a;
walk b
in
List.iter (fun r -> walk r.body) g;
List.filter_map
(fun r ->
if r.name = start || Hashtbl.mem used r.name then None else Some r.name)
g
let rec show_expr = function
| Ref n -> n
| RefArg (n, a) -> n ^ "<" ^ a ^ ">"
| Term s -> "\"" ^ s ^ "\""
| Seq xs -> String.concat " , " (List.map show_expr xs)
| Alt xs -> String.concat " | " (List.map show_paren xs)
| Opt e -> "[ " ^ show_expr e ^ " ]"
| Rep e -> "{ " ^ show_expr e ^ " }"
| Except (a, b) -> show_paren a ^ " - " ^ show_paren b
and show_paren e =
match e with Alt _ | Seq _ -> "( " ^ show_expr e ^ " )" | _ -> show_expr e
let show_rule r =
let ps =
if r.params = [] then "" else "<" ^ String.concat ", " r.params ^ ">"
in
r.name ^ ps ^ " = " ^ show_expr r.body ^ " ;"
(* ------------------------------------------------------------------ *)
(* 단일화 *)
(* *)
(* 매개변수 프로덕션을 실제로 쓰인 인자별로 펼친다. primary<ok>와 *)
(* primary<no>가 각각 하나의 평범한 프로덕션이 되고, 그 뒤 분석은 매개변수를 *)
(* 몰라도 된다. 문서는 짧게 유지하고 기계는 펼친 것을 본다. *)
(* ------------------------------------------------------------------ *)
let mangle n a = n ^ "<" ^ a ^ ">"
let expand (g : t) : t =
(* 인자를 머리에 박아 특수화한 규칙이 있으면 그것을 먼저 쓴다.
ident_or_struct<yes>와 ident_or_struct<no>처럼 인자에 따라 몸통이
달라지는 자리를 위한 것이다. 없으면 일반 규칙에 인자를 대입한다. *)
let by_name_arg n a =
match List.find_opt (fun r -> r.name = n && r.params = [ a ]) g with
| Some r -> Some r
| None -> List.find_opt (fun r -> r.name = n && r.params <> []) g
in
let out = Hashtbl.create 64 in
let queue = ref [] in
(* 인자를 실제 값으로 바꾸며 몸통을 복사한다 *)
let rec subst (env : (string * string) list) e =
match e with
| Term _ -> e
(* 매개변수 이름이 그대로 참조된 자리도 인자로 바꾼다: list<item>의 item *)
| Ref n -> ( match List.assoc_opt n env with Some v -> Ref v | None -> e)
| RefArg (n, a) -> (
let a = match List.assoc_opt a env with Some v -> v | None -> a in
match by_name_arg n a with
| Some r when r.params <> [] ->
let key = (n, a) in
if (not (Hashtbl.mem out (mangle n a))) && not (List.mem key !queue)
then queue := key :: !queue;
Ref (mangle n a)
| _ -> Ref n)
| Seq xs -> Seq (List.map (subst env) xs)
| Alt xs -> Alt (List.map (subst env) xs)
| Opt x -> Opt (subst env x)
| Rep x -> Rep (subst env x)
| Except (x, y) -> Except (subst env x, subst env y)
in
(* 매개변수 없는 규칙부터 *)
List.iter
(fun r ->
if r.params = [] then
Hashtbl.replace out r.name { r with body = subst [] r.body })
g;
while !queue <> [] do
let n, a = List.hd !queue in
queue := List.tl !queue;
let key = mangle n a in
if not (Hashtbl.mem out key) then
match by_name_arg n a with
| None -> ()
| Some r ->
let env = match r.params with p :: _ -> [ (p, a) ] | [] -> [] in
Hashtbl.replace out key
{ name = key; params = []; body = subst env r.body; line = r.line }
done;
(* 원본 순서를 최대한 유지한다 — 문서와 대조하기 쉽게 *)
let ordered =
List.concat_map
(fun r ->
if r.params = [] then
match Hashtbl.find_opt out r.name with Some x -> [ x ] | None -> []
else
Hashtbl.fold
(fun k v acc ->
if
String.length k > String.length r.name
&& String.sub k 0 (String.length r.name + 1) = r.name ^ "<"
then v :: acc
else acc)
out []
|> List.sort (fun a b -> compare a.name b.name))
g
in
ordered
(* ------------------------------------------------------------------ *)
(* nullable과 FIRST *)
(* *)
(* 여기서부터가 "다음 한 토큰만 보고 결정할 수 있는가"를 기계가 판정하는 *)
(* 근거다. 문법 첫머리의 LL(1) 주장은 지금까지 사람의 말이었다. *)
(* ------------------------------------------------------------------ *)
(* 단말 하나의 이름. 리터럴은 그 철자, 토큰 부류는 그 이름. *)
module SS = Set.Make (String)
type analysis = {
rules : t;
tokens : SS.t; (* 단말로 취급할 Ref 이름 (ident, NEWLINE 등) *)
nullable : (string, bool) Hashtbl.t;
first : (string, SS.t) Hashtbl.t;
}
let is_token a n = SS.mem n a.tokens || find a.rules n = None
let rec nullable_expr a = function
| Term _ -> false
| Ref n ->
if is_token a n then false else Hashtbl.find_opt a.nullable n = Some true
| RefArg (n, x) -> nullable_expr a (Ref (mangle n x))
| Seq xs -> List.for_all (nullable_expr a) xs
| Alt xs -> List.exists (nullable_expr a) xs
| Opt _ | Rep _ -> true
| Except (x, _) -> nullable_expr a x
let rec first_expr a = function
| Term s -> SS.singleton s
| Ref n -> (
if is_token a n then SS.singleton n
else
match Hashtbl.find_opt a.first n with Some s -> s | None -> SS.empty)
| RefArg (n, x) -> first_expr a (Ref (mangle n x))
| Alt xs ->
List.fold_left (fun acc x -> SS.union acc (first_expr a x)) SS.empty xs
| Opt x | Rep x -> first_expr a x
| Except (x, _) -> first_expr a x
| Seq xs ->
let rec go acc = function
| [] -> acc
| x :: rest ->
let acc = SS.union acc (first_expr a x) in
if nullable_expr a x then go acc rest else acc
in
go SS.empty xs
(* 변화가 없을 때까지 돈다. 문법은 작으므로 단순한 고정점으로 충분하다. *)
let analyze ?(tokens = []) (g : t) : analysis =
let a =
{
rules = g;
tokens = SS.of_list tokens;
nullable = Hashtbl.create 64;
first = Hashtbl.create 64;
}
in
List.iter (fun r -> Hashtbl.replace a.nullable r.name false) g;
List.iter (fun r -> Hashtbl.replace a.first r.name SS.empty) g;
let changed = ref true in
while !changed do
changed := false;
List.iter
(fun r ->
let nu = nullable_expr a r.body in
if nu && Hashtbl.find_opt a.nullable r.name <> Some true then (
Hashtbl.replace a.nullable r.name true;
changed := true);
let f = first_expr a r.body in
let old =
match Hashtbl.find_opt a.first r.name with
| Some s -> s
| None -> SS.empty
in
if not (SS.equal f old) then (
Hashtbl.replace a.first r.name (SS.union old f);
changed := true))
g
done;
a
let first a name =
match Hashtbl.find_opt a.first name with Some s -> s | None -> SS.empty
let nullable a name = Hashtbl.find_opt a.nullable name = Some true
(* ------------------------------------------------------------------ *)
(* FOLLOW와 LL(1) 충돌 *)
(* ------------------------------------------------------------------ *)
(* 이어지는 자리를 (올 수 있는 단말들, 규칙 끝에 닿을 수 있는가)로 나른다.
끝에 닿을 수 있으면 그 규칙의 FOLLOW가 더해진다. *)
type follow_env = {
a : analysis;
fol : (string, SS.t) Hashtbl.t;
mutable deps : (string * string) list;
(* (n, owner): follow n ⊇ follow owner *)
}
let get_fol e n =
match Hashtbl.find_opt e.fol n with Some s -> s | None -> SS.empty
let rec collect e owner expr (cont : SS.t) (cont_end : bool) =
match expr with
| Term _ -> ()
| RefArg (n, x) -> collect e owner (Ref (mangle n x)) cont cont_end
| Ref n ->
if not (is_token e.a n) then begin
Hashtbl.replace e.fol n (SS.union (get_fol e n) cont);
if cont_end && not (List.mem (n, owner) e.deps) then
e.deps <- (n, owner) :: e.deps
end
| Alt xs -> List.iter (fun x -> collect e owner x cont cont_end) xs
| Opt x -> collect e owner x cont cont_end
(* 반복은 자기 자신이 뒤따를 수 있다 *)
| Rep x -> collect e owner x (SS.union cont (first_expr e.a x)) cont_end
| Except (x, _) -> collect e owner x cont cont_end
| Seq xs ->
let acc_first = ref cont and acc_end = ref cont_end in
List.iter
(fun x ->
collect e owner x !acc_first !acc_end;
let f = first_expr e.a x in
if nullable_expr e.a x then acc_first := SS.union f !acc_first
else (
acc_first := f;
acc_end := false))
(List.rev xs)
let follows ?(tokens = []) (g : t) : (string, SS.t) Hashtbl.t =
let a = analyze ~tokens g in
let e = { a; fol = Hashtbl.create 64; deps = [] } in
List.iter (fun r -> collect e r.name r.body SS.empty true) g;
(* 규칙 끝에 닿는 참조는 그 규칙의 FOLLOW를 물려받는다. 고정점. *)
let changed = ref true in
while !changed do
changed := false;
List.iter
(fun (n, owner) ->
let merged = SS.union (get_fol e n) (get_fol e owner) in
if not (SS.equal merged (get_fol e n)) then (
Hashtbl.replace e.fol n merged;
changed := true))
e.deps
done;
e.fol
type conflict = {
c_rule : string;
c_line : int;
c_kind : string; (* "선택" | "선택적" | "반복" *)
c_tokens : string list; (* 겹치는 단말 *)
c_detail : string;
(* greedy 규칙으로 해소되는가. [ X ]와 { X }가 "최대한 먹는다"로 정의되면,
겹치는 토큰이 흡수 대상뿐일 때 결정이 갈린다. 어느 쪽으로 읽든 같은
것을 뜻하는 자리에서만 쓸 수 있는 해소다 — 진짜 중의성을 덮지 않도록
greedy 토큰 목록은 문법이 명시한다. *)
c_greedy : bool;
}
(* 같은 단말로 시작하는 대안이 둘 이상이면 한 토큰으로 결정할 수 없다. *)
let conflicts ?(tokens = []) ?(greedy = []) (g : t) : conflict list =
let a = analyze ~tokens g in
let fol = follows ~tokens g in
let out = ref [] in
let add r kind toks detail =
if toks <> [] then
out :=
{
c_rule = r.name;
c_line = r.line;
c_kind = kind;
c_tokens = toks;
c_detail = detail;
(* 선택/반복만 greedy로 해소된다. 대안(Alt) 충돌은 못 덮는다 *)
c_greedy =
kind <> "선택" && List.for_all (fun t -> List.mem t greedy) toks;
}
:: !out
in
let rec walk r expr (cont : SS.t) (cont_end : bool) =
let cont_full =
if cont_end then
SS.union cont
(match Hashtbl.find_opt fol r.name with
| Some s -> s
| None -> SS.empty)
else cont
in
match expr with
| Term _ | Ref _ | RefArg _ -> ()
| Except (x, _) -> walk r x cont cont_end
| Alt xs ->
let n = List.length xs in
for i = 0 to n - 1 do
for j = i + 1 to n - 1 do
let fi = first_expr a (List.nth xs i)
and fj = first_expr a (List.nth xs j) in
let inter = SS.inter fi fj in
if not (SS.is_empty inter) then
add r "선택" (SS.elements inter)
(Printf.sprintf "%d번째와 %d번째 대안이 같은 토큰으로 시작합니다: %s / %s" (i + 1)
(j + 1)
(show_expr (List.nth xs i))
(show_expr (List.nth xs j)))
done
done;
let nulls = List.filter (nullable_expr a) xs in
if List.length nulls > 1 then
add r "선택" [ "(빈 것)" ] "비어도 되는 대안이 둘 이상입니다";
List.iter (fun x -> walk r x cont cont_end) xs
| Opt x ->
let inter = SS.inter (first_expr a x) cont_full in
if not (SS.is_empty inter) then
add r "선택적" (SS.elements inter)
(Printf.sprintf "[ %s ]를 넣을지 말지가 다음 토큰으로 갈리지 않습니다" (show_expr x));
walk r x cont cont_end
| Rep x ->
let inter = SS.inter (first_expr a x) cont_full in
if not (SS.is_empty inter) then
add r "반복" (SS.elements inter)
(Printf.sprintf "{ %s }를 더 돌지 말지가 다음 토큰으로 갈리지 않습니다" (show_expr x));
walk r x (SS.union cont (first_expr a x)) cont_end
| Seq xs ->
let acc_first = ref cont and acc_end = ref cont_end in
List.iter
(fun x ->
walk r x !acc_first !acc_end;
let f = first_expr a x in
if nullable_expr a x then acc_first := SS.union f !acc_first
else (
acc_first := f;
acc_end := false))
(List.rev xs)
in
List.iter (fun r -> walk r r.body SS.empty true) g;
List.rev !out
+154
View File
@@ -0,0 +1,154 @@
(* 문법에서 문장을 만든다.
대조는 두 방향이 있다. 저장소의 .cool 파일로 하는 대조는 "사람이 쓴 코드"
만 훑으므로, 문법이 약속했는데 파서가 못 읽는 구석은 아무도 안 밟으면
드러나지 않는다. 여기서는 문법이 허용하는 문장을 직접 만들어 파서에
먹인다 — 파서가 거부하면 둘 중 하나가 틀린 것이다.
텍스트가 아니라 토큰 열을 만든다. 렉서의 줄바꿈 삽입 규칙을 거치면
문법이 허용해도 렉서가 만들 수 없는 문장이 생기는데, 그건 파서의 잘못이
아니다. 검사하려는 것은 문법과 파서 사이지 렉서가 아니다. *)
(* 최소 유도 길이. 깊이가 차면 가장 짧게 끝나는 가지를 고른다 —
이게 없으면 재귀 문법에서 생성이 끝나지 않는다. *)
let min_len (g : Ebnf.t) (tokens : string list) : (string, int) Hashtbl.t =
let tbl = Hashtbl.create 128 in
let inf = 1_000_000 in
List.iter (fun (r : Ebnf.rule) -> Hashtbl.replace tbl r.name inf) g;
let get n =
if
List.mem n tokens
|| not (List.exists (fun (r : Ebnf.rule) -> r.name = n) g)
then 1
else match Hashtbl.find_opt tbl n with Some v -> v | None -> 1
in
let cap a b = if a >= inf || b >= inf then inf else a + b in
let rec cost = function
| Ebnf.Term _ -> 1
| Ebnf.Ref n -> get n
| Ebnf.RefArg (n, x) -> get (Ebnf.mangle n x)
| Ebnf.Seq xs -> List.fold_left (fun a x -> cap a (cost x)) 0 xs
| Ebnf.Alt xs -> List.fold_left (fun a x -> min a (cost x)) inf xs
| Ebnf.Opt _ | Ebnf.Rep _ -> 0
| Ebnf.Except (x, _) -> cost x
in
let changed = ref true in
while !changed do
changed := false;
List.iter
(fun (r : Ebnf.rule) ->
let c = cost r.body in
if c < Hashtbl.find tbl r.name then (
Hashtbl.replace tbl r.name c;
changed := true))
g
done;
tbl
type gen = {
rules : (string, Ebnf.rule) Hashtbl.t;
costs : (string, int) Hashtbl.t;
tokens : string list;
mutable out : Token.kind list;
(* 깊이로 제한한다. 총 확장 횟수로 세면 선언 머리에서 다 써 버려 정작
식과 문에는 도달하지 못한다 — 재미있는 구석이 전부 그 안에 있는데. *)
mutable depth : int;
max_depth : int;
(* 어느 프로덕션을 밟았는가. 퍼저가 무엇을 시험하는지 모르면 통과했다는
말에 값이 없다 — 안 밟은 규칙은 시험되지 않은 규칙이다. *)
visited : (string, unit) Hashtbl.t;
}
let sample_token (n : string) : Token.kind =
match n with
| "ident" -> Token.Ident "x"
| "int_lit" -> Token.Int "1"
| "string_lit" -> Token.Str "s"
| "NEWLINE" -> Token.Newline
| _ -> Token.Ident "x"
(* 단말 철자에서 토큰으로. 모든 토큰을 훑어 show_kind가 같은 것을 찾는다 —
철자 표를 따로 두면 그것도 어긋난다 *)
let token_of_term (s : string) : Token.kind option =
List.find_opt (fun k -> Token.show_kind k = s) Token.all_kinds
let emit g k = g.out <- k :: g.out
let rec cost_of g = function
| Ebnf.Term _ -> 1
| Ebnf.Ref n -> (
if List.mem n g.tokens then 1
else match Hashtbl.find_opt g.costs n with Some v -> v | None -> 1)
| Ebnf.RefArg (n, x) -> cost_of g (Ebnf.Ref (Ebnf.mangle n x))
| Ebnf.Seq xs -> List.fold_left (fun a x -> a + cost_of g x) 0 xs
| Ebnf.Alt xs -> List.fold_left (fun a x -> min a (cost_of g x)) 1_000_000 xs
| Ebnf.Opt _ | Ebnf.Rep _ -> 0
| Ebnf.Except (x, _) -> cost_of g x
let deep g = g.depth >= g.max_depth
let rec gen_expr g (e : Ebnf.expr) =
match e with
| Ebnf.Term s -> (
match token_of_term s with
| Some k -> emit g k
| None -> emit g (Token.Ident "x"))
| Ebnf.Ref n -> (
if List.mem n g.tokens then emit g (sample_token n)
else
match Hashtbl.find_opt g.rules n with
| Some r ->
Hashtbl.replace g.visited n ();
g.depth <- g.depth + 1;
gen_expr g r.Ebnf.body;
g.depth <- g.depth - 1
| None -> emit g (sample_token n))
| Ebnf.RefArg (n, x) -> gen_expr g (Ebnf.Ref (Ebnf.mangle n x))
| Ebnf.Seq xs -> List.iter (gen_expr g) xs
| Ebnf.Alt xs ->
let pick =
if deep g then begin
(* 깊이가 차면 가장 짧게 끝나는 가지. 같은 값이 여럿이면 무작위로
고른다 — 늘 첫 번째를 고르면 뒤쪽 가지가 영영 안 밟힌다 *)
let best =
List.fold_left (fun a x -> min a (cost_of g x)) 1_000_000 xs
in
let cands = List.filter (fun x -> cost_of g x = best) xs in
match cands with
| [] -> None
| _ -> Some (List.nth cands (Random.int (List.length cands)))
end
else Some (List.nth xs (Random.int (List.length xs)))
in
Option.iter (gen_expr g) pick
| Ebnf.Opt x -> if (not (deep g)) && Random.bool () then gen_expr g x
| Ebnf.Rep x ->
if not (deep g) then
(* 얕을수록 더 돌린다. 최상위 { item }이 0번이면 빈 파일이 된다 *)
let n = if g.depth <= 1 then 1 + Random.int 3 else Random.int 3 in
for _ = 1 to n do
gen_expr g x
done
| Ebnf.Except (x, _) -> gen_expr g x
(* start에서 시작하는 문장 하나. 토큰 열을 돌려준다 (Eof 포함) *)
let sentence ?(tokens = []) ?(start = "module") ?(max_depth = 14) ?visited
(g : Ebnf.t) : Token.t list =
let g' = Ebnf.expand g in
let rules = Hashtbl.create 128 in
List.iter (fun (r : Ebnf.rule) -> Hashtbl.replace rules r.name r) g';
let st =
{
rules;
costs = min_len g' tokens;
tokens;
out = [];
depth = 0;
max_depth;
visited = (match visited with Some v -> v | None -> Hashtbl.create 8);
}
in
gen_expr st (Ebnf.Ref start);
let pos = Token.{ line = 1; col = 1 } in
List.rev_map (fun k -> Token.{ kind = k; pos }) st.out |> fun xs ->
List.rev (Token.{ kind = Token.Eof; pos } :: List.rev xs)
+9 -1
View File
@@ -25,6 +25,9 @@ let is_exported = function
| I_capability { pub; _ } -> pub
| I_const { pub; _ } -> pub
| I_reexport _ -> true
(* 테스트는 표면이 아니다. 테스트를 고쳤다고 downstream이 재검사되면
안 된다 — 함수 본문과 같은 이유다 *)
| I_test _ -> false
| I_import _ -> false
let strip = function
@@ -38,6 +41,7 @@ let item_name = function
| I_capability { name; _ } -> name
| I_const { name; _ } -> name
| I_reexport { name; _ } -> name
| I_test { name; _ } -> name
| I_import { alias; _ } -> alias
(* reexport는 이름이 아니라 해소된 정의 본문이 hash에 들어간다.
@@ -104,7 +108,11 @@ let rec q_ty alias defined gen (t : ty) : ty =
T_fn
{
affine;
params = List.map (q_ty alias defined gen) params;
params =
List.map
(fun (p : fn_param_ty) ->
{ p with pt_ty = q_ty alias defined gen p.pt_ty })
params;
eff;
ret = Option.map (q_ty alias defined gen) ret;
pos;
+219 -8
View File
@@ -81,8 +81,46 @@ let bind (env : env) n v : env =
(* 런타임이 제공하는 것 *)
(* ------------------------------------------------------------------ *)
(* 문자열 도우미. 언어에 인덱싱 연산자가 없으므로 이 일은 런타임 몫이다. *)
let find_sub hay needle =
let n = String.length needle and h = String.length hay in
let rec go i =
if i + n > h then None
else if String.sub hay i n = needle then Some i
else go (i + 1)
in
if n = 0 then Some 0 else go 0
let split_on s sep =
if sep = "" then [ s ]
else
let n = String.length sep in
let rec go s acc =
match find_sub s sep with
| None -> List.rev (s :: acc)
| Some i ->
go
(String.sub s (i + n) (String.length s - i - n))
(String.sub s 0 i :: acc)
in
go s []
let out = Buffer.create 1024
(* 프로그램 인자. 런타임이 들고 있다가 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" ->
@@ -96,20 +134,121 @@ let root_capability name : value option =
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)
@@ -181,6 +320,8 @@ let rec eval st (env : env) (e : Ir.t) : value =
| [ 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)
@@ -228,6 +369,38 @@ and apply st pos f args =
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))
@@ -313,13 +486,17 @@ and eval_binary st env op a b pos =
(* main이 선언한 capability만 런타임이 넘긴다. 선언하지 않은 권한은
프로그램 안에 존재하지 않는다. *)
let run (prog : Ir.program) (entry : string)
(main_params : (string * string) list) : (string, Token.pos * string) result
=
(* 실패해도 그때까지 나온 출력을 함께 돌려준다.
print는 실제로 일어난 effect다. 일어난 일을 안 보여주면 "어디까지 갔나"를
알 수 없고, 그게 실패했을 때 가장 먼저 보고 싶은 것이다. *)
let run ?(args = []) (prog : Ir.program) (entry : string)
(main_params : (string * string) list) :
string * (Token.pos * string) option =
Buffer.clear out;
argv := args;
let st = { prog } in
match Hashtbl.find_opt prog.Ir.fns (entry ^ "#main") with
| None -> Error (Token.{ line = 0; col = 0 }, "main 함수가 없습니다")
| None -> ("", Some (Token.{ line = 0; col = 0 }, "main 함수가 없습니다"))
| Some fn -> (
let args =
List.map
@@ -331,12 +508,46 @@ let run (prog : Ir.program) (entry : string)
main_params
in
match List.find_opt Result.is_error args with
| Some (Error m) -> Error (Token.{ line = 0; col = 0 }, m)
| Some (Error m) -> ("", Some (Token.{ line = 0; col = 0 }, m))
| _ -> (
let args = List.map Result.get_ok args in
try
ignore (apply st Token.{ line = 0; col = 0 } (VFn fn) args);
Ok (Buffer.contents out)
(Buffer.contents out, None)
with
| Fail (pos, msg) -> Error (pos, msg)
| Return_exc _ -> Ok (Buffer.contents out)))
| 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 })
+13 -2
View File
@@ -45,6 +45,7 @@ type t =
| 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
@@ -60,6 +61,8 @@ 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 -> 메서드 이름 *)
@@ -183,6 +186,7 @@ let rec lower_pat c (p : Ast.pattern) : pat =
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)
@@ -190,12 +194,12 @@ let rec lower c (e : Ast.expr) : t =
I_make (name, List.map (fun (n, e) -> (n, lower c e)) fields)
| Ast.E_closure cl ->
lpush c;
List.iter (fun (n, _) -> lbind c n) cl.cl_params;
List.iter (fun (p : Ast.cl_param) -> lbind c p.cp_name) cl.cl_params;
let body = lower_block c cl.cl_body in
lpop c;
I_closure
{
c_params = List.map fst cl.cl_params;
c_params = List.map (fun (p : Ast.cl_param) -> p.cp_name) cl.cl_params;
c_body = body;
c_pos = cl.cl_pos;
}
@@ -310,6 +314,7 @@ 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;
@@ -377,6 +382,12 @@ let of_program (mods : modinfo list) : program =
| 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;
+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;
]
+91 -15
View File
@@ -27,12 +27,17 @@ type binding = {
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;
@@ -41,6 +46,10 @@ type state = {
mutable depth : int;
(* 클로저 프레임: (프레임 깊이, 잡아온 바깥 바인딩). 중첩 클로저를 위해 스택 *)
mutable frames : (int * binding list ref) list;
(* 클로저 인자를 걸을 때 기대 파라미터 타입. move 검사는 타입 검사와 별도
순회라 타입을 모르는데, 클로저 파라미터의 affinity를 알아야 빌림 여부를
판정할 수 있다. 호출 대상의 시그니처에서 읽어와 여기 잠깐 둔다. *)
mutable cl_expect : fn_param_ty list option;
mutable errors : error list;
}
@@ -74,7 +83,10 @@ let derive_affinity st (items : item list) =
(fun it ->
match it with
| I_capability { name; _ } -> Hashtbl.replace st.aff name true
| I_struct { name; _ } -> Hashtbl.replace st.aff name false
| I_struct { name; fields; _ } ->
Hashtbl.replace st.aff name false;
Hashtbl.replace st.fields name
(List.map (fun f -> (f.f_name, f.f_ty)) fields)
| I_enum { name; _ } -> Hashtbl.replace st.aff name false
| _ -> ())
items;
@@ -122,7 +134,7 @@ let derive_affinity st (items : item list) =
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_ =
let add st name ~affine ~use ~mut_ ?ty () =
st.next_id <- st.next_id + 1;
let b =
{
@@ -132,6 +144,7 @@ let add st name ~affine ~use ~mut_ =
b_use = use;
b_mut = mut_;
b_depth = st.depth;
b_ty = ty;
}
in
(match st.scopes with
@@ -184,9 +197,29 @@ let merge st 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
@@ -264,7 +297,7 @@ let rec walk st (ctx : ctx) (e : expr) : vinfo =
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);
ignore (add st name ~affine:true ~use:true ~mut_:false ());
let r = walk_block st ctx body in
pop st;
r
@@ -274,11 +307,26 @@ let rec walk st (ctx : ctx) (e : expr) : vinfo =
pop st;
r
| E_call { callee; args; pos } -> walk_call st callee args pos
| E_field { obj; _ } ->
(* v0에 부분 move는 없다. 필드 접근은 빌림이고 결과도 빌린 값이다. *)
| E_field { obj; name; pos } ->
(* v0에 부분 move는 없다. 필드 접근은 빌림이고 결과도 빌린 값이다.
그러므로 Move 자리에 놓으면 오류다 — 이 검사가 없으면 capability를
struct에 넣고 필드를 두 번 읽어 복제할 수 있다 (보안 정리 ii). *)
let o = walk st Borrow obj in
if o.v_affine || o.v_use then { v_affine = true; v_use = true }
else v_copy
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
@@ -291,7 +339,7 @@ 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)
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 =
@@ -310,7 +358,7 @@ and walk_stmt st = function
let rec bind p =
match p with
| P_bind (n, _) ->
ignore (add st n ~affine:info.v_affine ~use:info.v_use ~mut_)
ignore (add st n ~affine:info.v_affine ~use:info.v_use ~mut_ ())
| P_ctor { args; _ } -> List.iter bind args
| _ -> ()
in
@@ -330,10 +378,24 @@ and walk_closure st (c : closure) : vinfo =
let acc = ref [] in
st.frames <- (st.depth, acc) :: st.frames;
push st;
List.iter
(fun (n, ann) ->
let affine = match ann with Some t -> ty_affine st t | None -> false in
ignore (add st n ~affine ~use:false ~mut_:false))
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;
@@ -368,6 +430,9 @@ and walk_call st callee args pos =
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
@@ -421,7 +486,7 @@ let check_fn st (d : fn_decl) =
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))
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
@@ -437,6 +502,7 @@ 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 = [];
@@ -444,6 +510,7 @@ let check ?(imports : item list = []) (m : modul) : error list =
next_id = 0;
depth = 0;
frames = [];
cl_expect = None;
errors = [];
}
in
@@ -463,7 +530,16 @@ let check ?(imports : item list = []) (m : modul) : error list =
| _ -> ())
(imports @ m.items);
List.iter
(fun it -> match it with I_fn { decl; _ } -> check_fn st decl | _ -> ())
(fun it ->
match it with
| I_fn { decl; _ } -> check_fn st decl
(* 테스트도 같은 검사를 받는다 *)
| I_test { body; _ } ->
st.scopes <- [];
push st;
ignore (walk_block st (Move "반환할 수") body);
pop st
| _ -> ())
m.items;
List.sort
(fun a b ->
+28 -14
View File
@@ -145,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)
@@ -398,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
@@ -443,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
@@ -564,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
(* ------------------------------------------------------------------ *)
@@ -764,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
+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)
+12 -6
View File
@@ -104,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
@@ -154,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 ()
@@ -169,9 +170,9 @@ let rec resolve_expr st = function
| 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 -> ()
@@ -261,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 -> ()
@@ -350,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
@@ -378,6 +379,11 @@ 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 표면 문제다 —
+33 -9
View File
@@ -65,9 +65,18 @@ let create ?(root = ".") ?std () =
패키지 참조다 (cool.dev/std/list). v0에는 패키지 해소가 없으므로 그런
import는 불투명하게 남는다 — 없다고 말하지 않는다. *)
let is_package path =
match String.index_opt path '/' with
| Some i -> String.contains (String.sub path 0 i) '.'
| None -> String.contains 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/"
@@ -258,14 +267,15 @@ let main_params (m : Ast.modul) =
| _ -> [])
m.items
let run st path : (string, error) result =
(* 출력과 실패를 함께 돌려준다. 실패해도 그때까지 나온 것은 보여줘야 한다 *)
let run ?(args = []) st path : string * error option =
load st path;
let errs = errors st in
if errs <> [] then Error (List.hd errs)
if errs <> [] then ("", Some (List.hd errs))
else
match find st path with
| None ->
Error { file = path; line = 0; col = 0; message = "모듈을 찾을 수 없습니다" }
("", Some { file = path; line = 0; col = 0; message = "모듈을 찾을 수 없습니다" })
| Some e -> (
(* IR은 그래프 전체를 받는다. 별칭이 실행 의미에 남지 않도록. *)
let mods =
@@ -275,6 +285,20 @@ let run st path : (string, error) result =
st.modules []
in
let prog = Ir.of_program mods in
match Interp.run prog path (main_params e.ast) with
| Ok out -> Ok out
| Error (pos, msg) -> Error (err_of path pos msg))
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 어휘 절).
값으로 끝날 수 있는 토큰 뒤에서만 삽입하므로, 연산자나 여는 괄호로
끝나는 줄은 다음 줄로 이어진다. *)
+106 -15
View File
@@ -14,7 +14,8 @@ type error = { pos : Token.pos; msg : string }
type scheme = {
s_gen : string list; (* 타입 파라미터 *)
s_eff_gen : string list; (* effect 파라미터 *)
s_params : T.t list;
(* (own, 타입). 무표기는 빌림 *)
s_params : (bool * T.t) list;
s_eff : T.eff; (* 이 함수를 부르면 수행되는 effect *)
s_ret : T.t;
}
@@ -43,6 +44,26 @@ type env = {
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))
@@ -102,7 +123,10 @@ let rec conv env (gen : string list) (t : Ast.ty) : T.t =
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);
}
@@ -121,7 +145,7 @@ let scheme_of env (d : fn_decl) : scheme =
{
s_gen = gen;
s_eff_gen = egen;
s_params = List.map (fun p -> conv env gen p.p_ty) d.fn_params;
s_params = List.map (fun p -> (p.p_own, conv env gen p.p_ty)) d.fn_params;
s_eff =
(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);
@@ -131,14 +155,14 @@ let scheme_of env (d : fn_decl) : scheme =
let instantiate (s : scheme) =
let sub = List.map (fun v -> (v, T.fresh ())) s.s_gen in
let esub = List.map (fun v -> (v, T.fresh_eff ())) s.s_eff_gen in
( List.map (T.subst sub esub) s.s_params,
( List.map (fun (o, t) -> (o, T.subst sub esub t)) s.s_params,
T.subst_eff esub s.s_eff,
T.subst 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
let esub = List.map (fun v -> (v, T.fresh_eff ())) s.s_eff_gen in
( List.map (T.subst sub esub) s.s_params,
( List.map (fun (o, t) -> (o, T.subst sub esub t)) s.s_params,
T.subst_eff esub s.s_eff,
T.subst sub esub s.s_ret )
@@ -164,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
@@ -358,20 +388,31 @@ 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
@@ -420,7 +461,14 @@ and infer_call env callee args pos =
| None -> (
match builtin_ctor n with
| Some (params, ret) ->
Some (T.TFn { affine = false; params; eff = []; ret })
Some
(T.TFn
{
affine = false;
params = List.map (fun t -> (false, t)) params;
eff = [];
ret;
})
| None -> (
match Hashtbl.find_opt env.fns n with
| Some s ->
@@ -448,7 +496,7 @@ 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)
@@ -481,7 +529,7 @@ 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
@@ -640,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 =
@@ -662,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)
(* ------------------------------------------------------------------ *)
(* 모듈 *)
@@ -776,6 +834,18 @@ let check ?(imports : item list = []) (m : modul) : error list =
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 ->
@@ -823,6 +893,27 @@ let check ?(imports : item list = []) (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
+21 -5
View File
@@ -10,13 +10,19 @@
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; eff : eff; ret : t }
(* params의 bool은 소유권이다 (own이면 true). affinity와 다른 축이다 —
affinity는 타입의 성질이고 소유권은 이 자리가 값을 가져가는가다. *)
| TFn of { affine : bool; params : (bool * t) list; eff : eff; ret : t }
| TMeta of meta ref
and meta = Unbound of int | Bound of t
@@ -97,6 +103,7 @@ let eff_missing ~declared ~performed =
let rec show t =
match resolve t with
| TUnknown -> "?"
| TNever -> "Never"
| TInt -> "Int"
| TBool -> "Bool"
| TString -> "String"
@@ -106,7 +113,8 @@ let rec show t =
| TCon (n, args) -> n ^ "[" ^ String.concat ", " (List.map show args) ^ "]"
| 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)
@@ -117,7 +125,8 @@ 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
(* 결정 위치의 해소: 파라미터의 effect 자리에 단독으로 선 미지수만 묶는다.
@@ -136,6 +145,8 @@ let unify_eff a b =
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
@@ -150,8 +161,13 @@ 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
&& 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
@@ -164,7 +180,7 @@ let rec subst tenv eenv t =
TFn
{
affine = f.affine;
params = List.map (subst tenv eenv) f.params;
params = List.map (fun (o, t) -> (o, subst tenv eenv t)) f.params;
eff = subst_eff eenv f.eff;
ret = subst tenv eenv f.ret;
}
+166
View File
@@ -0,0 +1,166 @@
// 설정 파일 파서.
//
// 이 파일은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴 것이다.
// 그것이 이 파일의 목적이다 — 언어가 실제로 쓸 만한지는 이런 코드에서만
// 드러난다.
import "cool.dev/std/list" as List
import "cool.dev/std/string" as String
import "cool.dev/std/int" as Int
import "cool.dev/std/option" as Option
import "cool.dev/std/result" as Result
import "cool.dev/std/test" as Test
// 설정 값. 타입이 셋뿐이므로 열거형이 맞다.
pub enum Value {
Text(String),
Number(Int),
Flag(Bool),
}
pub copyable struct Entry {
line: Int,
key: String,
value: Value,
}
// 오류에 줄 번호가 있어야 사람이 고칠 수 있다.
pub copyable struct Problem {
line: Int,
message: String,
}
pub copyable struct Config {
entries: List[Entry],
problems: List[Problem],
}
// 값 하나를 읽는다. 따옴표도 타입 표기도 없다 — 모양으로 정한다.
// true/false는 Flag, 정수로 읽히면 Number, 나머지는 Text.
pub fn parse_value(raw: String) -> Value {
if raw == "true" {
Flag(true)
} else if raw == "false" {
Flag(false)
} else {
match Int.parse(raw) {
Ok(n) => Number(n),
Err(_) => Text(raw),
}
}
}
pub fn show_value(v: Value) -> String {
match v {
Text(s) => String.concat("\"", String.concat(s, "\"")),
Number(n) => Int.show(n),
Flag(b) => if b { "true" } else { "false" },
}
}
pub fn type_name(v: Value) -> String {
match v {
Text(_) => "text",
Number(_) => "number",
Flag(_) => "flag",
}
}
// 한 줄을 읽는다. 빈 줄과 주석은 값이 없고, 그것은 오류가 아니다.
// 그래서 Result가 아니라 Option[Result[...]]가 필요해 보이지만, 그러면
// 부르는 쪽이 두 겹을 벗겨야 한다. Config 하나에 둘 다 모으는 편이 낫다.
pub fn parse_line(cfg: Config, no: Int, raw: String) -> Config {
let line = String.trim(raw)
let parts = String.split(line, "=")
if String.is_empty(line) {
cfg
} else if String.starts_with(line, "#") {
cfg
} else if List.len(parts) != 2 {
add_problem(cfg, no, String.concat("= 가 하나여야 합니다: ", line))
} else {
let key = String.trim(Option.unwrap_or(List.nth(parts, 0), ""))
let val = String.trim(Option.unwrap_or(List.nth(parts, 1), ""))
if String.is_empty(key) {
add_problem(cfg, no, "이름이 비어 있습니다")
} else {
add_entry(cfg, Entry { line: no, key: key, value: parse_value(val) })
}
}
}
pub fn add_entry(cfg: Config, e: Entry) -> Config {
Config {
entries: List.push(cfg.entries, e),
problems: cfg.problems,
}
}
pub fn add_problem(cfg: Config, no: Int, msg: String) -> Config {
Config {
entries: cfg.entries,
problems: List.push(cfg.problems, Problem { line: no, message: msg }),
}
}
pub fn parse(text: String) -> Config {
let lines = List.enumerate(String.split(text, "\n"))
let empty = Config { entries: [], problems: [] }
List.fold(lines, empty, fn(own cfg, l) {
parse_line(cfg, l.i + 1, l.value)
})
}
// 이름으로 찾는다. 없으면 Err — 못 찾은 것은 오류지 빈 값이 아니다.
pub fn lookup(cfg: Config, key: String) -> Result[Value, String] {
let hit = List.filter(cfg.entries, fn(e) { e.key == key })
Result.map(
Option.ok_or(List.first(hit), String.concat("설정에 없습니다: ", key)),
fn(e) { e.value },
)
}
pub fn get_int(cfg: Config, key: String) -> Result[Int, String] {
match lookup(cfg, key)? {
Number(n) => Ok(n),
Text(_) => Err(String.concat(key, "은(는) 정수가 아닙니다")),
Flag(_) => Err(String.concat(key, "은(는) 정수가 아닙니다")),
}
}
pub fn get_flag(cfg: Config, key: String) -> Result[Bool, String] {
match lookup(cfg, key)? {
Flag(b) => Ok(b),
Text(_) => Err(String.concat(key, "은(는) 참거짓이 아닙니다")),
Number(_) => Err(String.concat(key, "은(는) 참거짓이 아닙니다")),
}
}
// ------------------------------------------------------------------
// 테스트. 일반 코드와 같은 검사를 받고, capability를 받지 않으므로
// effect-free임이 증명된다 — 파일도 시계도 못 건드린다.
// ------------------------------------------------------------------
test "값의 타입은 모양으로 정한다" {
Test.assert(type_name(parse_value("true")) == "flag")
Test.assert(type_name(parse_value("42")) == "number")
Test.assert(type_name(parse_value("hello")) == "text")
}
test "빈 줄과 주석은 항목이 아니다" {
let cfg = parse("\n# 주석\n\n")
Test.assert(List.is_empty(cfg.entries))
Test.assert(List.is_empty(cfg.problems))
}
test "= 가 하나가 아니면 문제로 기록한다" {
let cfg = parse("a = 1\nb = c = d\n")
Test.assert(List.len(cfg.entries) == 1)
Test.assert(List.len(cfg.problems) == 1)
}
test "없는 이름을 찾으면 Err다" {
let cfg = parse("a = 1\n")
Test.assert(!Result.is_ok(get_int(cfg, "없음")))
Test.assert(Result.is_ok(get_int(cfg, "a")))
}
+11
View File
@@ -0,0 +1,11 @@
# 예제 설정
name = coollang
threads = 4
port = 8080
verbose = true
greeting = hello world
# 아래 두 줄은 일부러 틀렸다
= 42
broken = a = b
+96
View File
@@ -0,0 +1,96 @@
// 설정 파일 리포트 도구.
//
// 파일을 읽고, 파싱하고, 문제를 보고하고, 요약을 낸다.
// 권한은 셋뿐이다 — 읽기(File), 인자(Args), 출력(Console). 그 밖의 일은
// 이 프로그램이 할 수 없다. 시그니처에 적힌 것이 이 프로그램의 전부다.
import "config" as Cfg
import "cool.dev/std/list" as List
import "cool.dev/std/string" as String
import "cool.dev/std/int" as Int
import "cool.dev/std/option" as Option
pub capability Console {
fn print(s: String) effects {Console.print}
}
pub capability File {
fn read(path: String) effects {File.read} -> Result[String, String]
}
pub capability Args {
fn all() effects {Args.all} -> List[String]
}
pub fn report(c: Console, cfg: Cfg.Config) effects {Console.print} {
c.print("설정")
List.each(cfg.entries, fn(e) {
c.print(String.join("", [
" ", e.key, " = ", Cfg.show_value(e.value),
" (", Cfg.type_name(e.value), ")",
]))
})
c.print("")
c.print(String.join("", [
"항목 ", Int.show(List.len(cfg.entries)),
"개, 문제 ", Int.show(List.len(cfg.problems)), "개",
]))
if List.is_empty(cfg.problems) {
c.print("문제 없음")
} else {
c.print("")
c.print("문제")
List.each(cfg.problems, fn(p) {
c.print(String.join("", [ " ", Int.show(p.line), "행: ", p.message ]))
})
}
}
// 타입이 있는 조회. 없거나 타입이 다르면 Err이고, 둘 다 사람이 읽을 수 있다.
pub fn check_known(c: Console, cfg: Cfg.Config) effects {Console.print} {
c.print("")
c.print("알려진 설정")
show_int(c, cfg, "threads")
show_int(c, cfg, "port")
show_flag(c, cfg, "verbose")
}
pub fn show_int(c: Console, cfg: Cfg.Config, key: String) effects {Console.print} {
match Cfg.get_int(cfg, key) {
Ok(n) => c.print(String.join("", [ " ", key, " = ", Int.show(n) ])),
Err(m) => c.print(String.concat(" ", m)),
}
}
pub fn show_flag(c: Console, cfg: Cfg.Config, key: String) effects {Console.print} {
match Cfg.get_flag(cfg, key) {
Ok(b) => c.print(String.join("", [
" ", key, " = ", if b { "true" } else { "false" },
])),
Err(m) => c.print(String.concat(" ", m)),
}
}
pub fn first_arg(xs: List[String]) -> Result[String, String] {
Option.ok_or(List.first(xs), "설정 파일 경로가 필요합니다")
}
pub fn main(c: Console, f: File, a: Args)
effects {Console.print, File.read, Args.all} {
match run(c, f, a) {
Ok(_) => unit,
Err(m) => c.print(String.concat("오류: ", m)),
}
}
pub fn run(c: Console, f: File, a: Args)
effects {Console.print, File.read, Args.all} -> Result[Unit, String] {
let path = first_arg(a.all())?
let text = f.read(path)?
let cfg = Cfg.parse(text)
report(c, cfg)
check_known(c, cfg)
Ok(unit)
}
+3
View File
@@ -2,3 +2,6 @@
pub fn show(n: Int) -> String
pub fn abs(n: Int) -> Int
// 실패할 수 있으므로 Result다. 예외도, 0을 돌려주는 관례도 없다.
pub fn parse(s: String) -> Result[Int, String]
+40 -2
View File
@@ -2,14 +2,39 @@
//
// 본문이 없다. 런타임이 구현하고, 이 파일은 그 계약을 말한다.
// 그래서 이 파일은 구현이 아니라 시험대다 — effect 다형성이 실제로 쓸 만한지가
// 여기서 결정된다. each map이 effect 변수 하나로 표현되지 않으면 규칙이
// 틀린 것이고, 그건 v1로 미룰 수 없는 발견이다.
// 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,
@@ -19,3 +44,16 @@ 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
+14
View File
@@ -1,4 +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 실패")
}
}
+6 -1
View File
@@ -3,4 +3,9 @@
(libraries coollang)
(deps
(glob_files %{workspace_root}/samples/*.cool)
(glob_files %{workspace_root}/std/*.cool)))
(glob_files %{workspace_root}/std/*.cool)
(glob_files %{workspace_root}/docs/*.ebnf)
(glob_files_rec %{workspace_root}/dogfoods/**)
(glob_files %{workspace_root}/samples/app/*)
(glob_files %{workspace_root}/samples/modules/*.cool)
(glob_files %{workspace_root}/samples/run/*.cool)))
+538 -8
View File
@@ -327,8 +327,9 @@ let () =
(parse_err "fn f(\n a: Int\n)" = Some "다중 줄 목록에는 후행 콤마가 필요합니다");
check "if 머리의 struct 리터럴"
(parse_err "fn f() {\n if H { a: 1 } {\n b\n }\n}" <> None);
check "대입 왼쪽 검사"
(parse_err "fn f() {\n g() = 1\n}" = Some "대입 왼쪽에는 변수나 필드만 올 수 있습니다");
(* 대입 왼쪽 제약은 파서가 아니라 이름 해소가 본다. 구문으로 가르면
ident 하나로 대입과 식이 갈리지 않아 문법이 LL(1)이 아니게 된다 *)
check "대입 왼쪽은 파서가 보지 않는다" (parse_err "fn f() {\n g() = 1\n}" = None);
check "한 줄에 문 두 개" (parse_err "fn f() {\n a b\n}" = Some "문 끝에 줄바꿈이 필요합니다")
(* --- 괄호 안에서는 struct 리터럴이 다시 허용된다 --- *)
@@ -392,6 +393,8 @@ let has_err src frag =
let () =
check "중복 정의" (has_err "fn f()\nfn f()" "두 번 정의");
(* 파서에서 옮겨온 검사. 구문으로 가르면 문법이 LL(1)이 아니게 된다 *)
check "대입 왼쪽" (has_err "fn f() {\n g() = 1\n}" "대입 왼쪽에는 변수나 필드만");
check "중복 파라미터" (has_err "fn f(a: Int, a: Int)" "파라미터 a");
check "중복 제네릭" (has_err "fn f[a, a]()" "제네릭 파라미터 a");
check "선언되지 않은 effect 변수" (has_err "fn f() effects e" "선언되지 않은 effect 변수 e");
@@ -1029,9 +1032,11 @@ let console =
let outputs ?(use = []) src expected =
match run_src (imports use ^ console ^ src) with
| Ok out -> out = expected
| Error e ->
Printf.printf " (실행 오류: %s)\n" (Session.string_of_error e);
| out, None -> out = expected
| out, Some e ->
Printf.printf " (실행 오류: %s / 그때까지 출력: %S)\n"
(Session.string_of_error e)
out;
false
let () =
@@ -1110,7 +1115,7 @@ let () =
"a\nb\nafter\n");
(* 권한은 런타임에서만 온다. main이 선언하지 않으면 존재하지 않는다. *)
check "선언하지 않은 capability는 실행 시점에도 없다"
(match run_src "pub fn main() { }" with Ok "" -> true | _ -> false);
(match run_src "pub fn main() { }" with "", None -> true | _ -> false);
check "런타임이 모르는 capability는 거절한다"
(match
run_src
@@ -1119,8 +1124,28 @@ let () =
}\n\n\
pub fn main(d: Db) effects {Db.read} -> Int { d.read() }"
with
| Error e -> has_sub e.message "제공하지 않습니다"
| Ok _ -> false)
| _, Some e -> has_sub e.message "제공하지 않습니다"
| _, None -> false)
(* 실패해도 그때까지의 출력은 남는다. print는 실제로 일어난 effect이고,
일어난 일을 안 보여주면 "어디까지 갔나"를 알 수 없다. *)
let () =
match
run_src
(imports [ "Int" ] ^ console
^ "pub fn risky(a: Int, b: Int) -> Int { a / b }\n\n\
pub fn main(c: Console) effects {Console.print} {\n\
\ c.print(\"\")\n\
\ c.print(Int.show(risky(10, 0)))\n\
\ c.print(\"\")\n\
}")
with
| out, Some e ->
check "0으로 나누면 실행 시점 오류다" (has_sub e.message "0으로 나눌 수 없습니다");
check "실패해도 그 전 출력은 남는다" (out = "\n")
| _, None ->
check "0으로 나누면 실행 시점 오류다" false;
check "실패해도 그 전 출력은 남는다" false
(* 표준 라이브러리 시그니처가 실제 검사에 쓰이는지.
@@ -1232,3 +1257,508 @@ let () =
in
List.exists (fun m -> has_sub m "가져왔지만 쓰지 않습니다") ms
&& List.exists (fun m -> has_sub m "String이(가) 필요한데 Int") ms)
(* ------------------------------------------------------------------ *)
(* 실제 프로그램 *)
(* *)
(* samples/app은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴 것이다. *)
(* 언어가 쓸 만한지는 이런 코드에서만 드러난다. *)
(* ------------------------------------------------------------------ *)
let () =
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
match
Session.run
~args:[ "../samples/app/example.conf" ]
st "../samples/app/main.cool"
with
| _, Some e ->
Printf.printf " (실행 오류: %s)\n" (Session.string_of_error e);
check "app: 설정 리포트가 돈다" false
| out, None ->
check "app: 항목과 문제를 센다" (has_sub out "항목 5개, 문제 2개");
check "app: 값의 타입을 모양으로 정한다"
(has_sub out "threads = 4 (number)"
&& has_sub out "verbose = true (flag)"
&& has_sub out "name = \"coollang\" (text)");
check "app: 문제에 줄 번호가 붙는다"
(has_sub out "10행: 이름이 비어 있습니다" && has_sub out "11행: = 가 하나여야 합니다");
check "app: 타입 있는 조회" (has_sub out "port = 8080")
let () =
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
match Session.run st "../samples/app/main.cool" with
| out, None -> check "app: 인자가 없으면 말해준다" (has_sub out "경로가 필요합니다")
| _, Some _ -> check "app: 인자가 없으면 말해준다" false
let () =
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
match Session.run ~args:[ "/없는/파일.conf" ] st "../samples/app/main.cool" with
| out, None -> check "app: 없는 파일을 Err로 돌려준다" (has_sub out "오류: ")
| _, Some _ -> check "app: 없는 파일을 Err로 돌려준다" false
(* std 선언과 런타임 구현이 어긋나면 검사는 통과하고 실행이 죽는다.
v0에서 둘은 다른 파일에 있으므로 일치는 테스트가 지킨다 (friction F7). *)
let () =
let dir = "../std" in
let declared =
Sys.readdir dir |> Array.to_list
|> List.filter (fun f -> Filename.check_suffix f ".cool")
|> List.concat_map (fun f ->
let m = ref "" in
(match Driver.ast (Filename.concat dir f) with
| Error _ -> ()
| Ok _ -> m := Filename.remove_extension f);
match Driver.ast (Filename.concat dir f) with
| Error _ -> []
| Ok ast ->
List.filter_map
(function
(* 본문이 있으면 coollang으로 구현된 것이다 (std/test.cool의
assert). 런타임 구현이 필요한 것은 본문 없는 선언뿐이다 *)
| Ast.I_fn { decl; _ } when decl.fn_body = None ->
Some (!m ^ "." ^ decl.fn_name)
| _ -> None)
ast.Ast.items)
in
check "std에 선언이 있다" (List.length declared > 20);
List.iter
(fun name ->
check
(Printf.sprintf "std 선언 %s에 런타임 구현이 있다" name)
(List.mem name Interp.implemented))
declared;
List.iter
(fun name ->
check
(Printf.sprintf "런타임 구현 %s에 std 선언이 있다" name)
(List.mem name declared))
Interp.implemented
(* ------------------------------------------------------------------ *)
(* 문법 파일 *)
(* *)
(* docs/grammar.ebnf는 이제 문서가 아니라 기계가 읽는 소스다. 여기서 *)
(* 검사하는 것 셋: *)
(* 1. 기계가 읽을 수 있는가 *)
(* 2. 정의되지 않은 이름이 토큰 부류뿐인가 *)
(* 3. LL(1)인가 — 문법 첫머리의 주장이 여기서 검증된다 *)
(* ------------------------------------------------------------------ *)
let () =
let src =
let ic = open_in_bin "../docs/grammar.ebnf" in
let n = in_channel_length ic in
let s = really_input_string ic n in
close_in ic;
s
in
match Ebnf.parse_result src with
| Error e ->
Printf.printf " (문법 파일 %d행: %s)\n" e.line e.msg;
check "grammar.ebnf를 읽을 수 있다" false
| Ok g ->
check "grammar.ebnf를 읽을 수 있다" true;
check "프로덕션이 충분히 있다" (List.length g > 50);
(* 어휘 층의 이름만 정의 없이 참조될 수 있다 *)
let allowed = [ "NEWLINE"; "char"; "digit"; "letter" ] in
let undef = Ebnf.undefined g in
List.iter
(fun n ->
check
(Printf.sprintf "grammar.ebnf: %s는 정의되었거나 토큰 부류다" n)
(List.mem n allowed))
undef;
check "grammar.ebnf에 죽은 프로덕션이 없다" (Ebnf.unreachable g ~start:"module" = []);
let g = Ebnf.expand g in
let tokens = allowed @ [ "ident"; "int_lit"; "string_lit" ] in
let real =
List.filter
(fun (c : Ebnf.conflict) -> not c.c_greedy)
(Ebnf.conflicts ~tokens ~greedy:[ "NEWLINE" ] g)
in
List.iter
(fun (c : Ebnf.conflict) ->
Printf.printf " (LL(1) 충돌 %s:%d [%s] %s — %s)\n" c.c_rule c.c_line
c.c_kind
(String.concat " " c.c_tokens)
c.c_detail)
real;
check "grammar.ebnf는 LL(1)이다" (real = [])
(* 문법 문서의 어휘 절은 코드에서 생성된다. 두 곳에 손으로 적힌 목록은
어긋난다 — 이 프로젝트가 그것으로 한 번 데었다. *)
let () =
let src =
let ic = open_in_bin "../docs/grammar.ebnf" in
let n = in_channel_length ic in
let s = really_input_string ic n in
close_in ic;
s
in
let block = Lexical_doc.render () in
check "문법 문서의 어휘 절이 token.ml과 일치한다" (has_sub src block);
if not (has_sub src block) then begin
print_endline " 현재 코드가 만드는 블록:";
print_endline block
end
(* ------------------------------------------------------------------ *)
(* 문법과 파서의 대조 *)
(* *)
(* 문법 파일에서 직접 읽는 인식기와 손으로 쓴 파서에 같은 토큰 열을 주고 *)
(* 판정이 같은지 본다. 갈리면 둘 중 하나가 틀린 것이고 빌드가 깨진다. *)
(* 이것이 설명서와 구현이 어긋나지 않게 하는 유일한 장치다 — *)
(* "문서를 잘 관리하자"는 이미 실패했다. *)
(* ------------------------------------------------------------------ *)
let () =
let read f =
let ic = open_in_bin f in
let n = in_channel_length ic in
let s = really_input_string ic n in
close_in ic;
s
in
let g =
match Ebnf.parse_result (read "../docs/grammar.ebnf") with
| Ok g -> g
| Error e -> failwith (Printf.sprintf "문법 %d행: %s" e.line e.msg)
in
let tokens =
[ "ident"; "int_lit"; "string_lit"; "NEWLINE"; "char"; "digit"; "letter" ]
in
let files =
List.concat_map
(fun dir ->
try
Sys.readdir dir |> Array.to_list
|> List.filter (fun f -> Filename.check_suffix f ".cool")
|> List.sort compare
|> List.map (Filename.concat dir)
with Sys_error _ -> [])
[
"../samples";
"../samples/modules";
"../samples/run";
"../samples/app";
"../std";
]
in
check "대조할 파일이 있다" (List.length files > 15);
List.iter
(fun f ->
match Lexer.lex_result (read f) with
| Error _ -> ()
| Ok toks ->
let hand = Result.is_ok (Parser.parse_result toks) in
let spec = Result.is_ok (Recognize.check ~tokens g toks) in
if hand <> spec then
Printf.printf " (%s: 파서 %s / 문법 %s)\n" f
(if hand then "받음" else "거부")
(if spec then "받음" else "거부");
check (Filename.basename f ^ ": 문법과 파서의 판정이 같다") (hand = spec))
files
(* 문법이 약속한 것을 파서가 실제로 읽는가.
저장소의 .cool 파일만으로는 사람이 쓴 코드만 훑는다 — 아무도 안 밟은
구석은 드러나지 않는다. 문법에서 문장을 만들어 파서에 먹인다.
커버리지를 같이 재는 이유: 안 밟은 규칙은 시험되지 않은 규칙이다. *)
let () =
let read f =
let ic = open_in_bin f in
let n = in_channel_length ic in
let s = really_input_string ic n in
close_in ic;
s
in
match Ebnf.parse_result (read "../docs/grammar.ebnf") with
| Error _ -> ()
| Ok g ->
let tokens =
[
"ident"; "int_lit"; "string_lit"; "NEWLINE"; "char"; "digit"; "letter";
]
in
let visited = Hashtbl.create 128 in
let bad = ref 0 in
let shown = ref 0 in
for i = 1 to 2500 do
Random.init i;
let toks = Ebnf_gen.sentence ~tokens ~visited ~max_depth:18 g in
match Parser.parse_result toks with
| Ok _ -> ()
| Error e ->
incr bad;
if !shown < 3 then begin
incr shown;
Printf.printf " (씨앗 %d: 문법은 만들었는데 파서가 거부 — %s)\n" i e.msg
end
done;
check "문법이 만든 문장을 파서가 전부 받는다" (!bad = 0);
let lexical =
[
"ident";
"int_lit";
"string_lit";
"str_char";
"escape";
"bool_lit";
"literal";
]
in
let all =
Ebnf.expand g
|> List.map (fun (r : Ebnf.rule) -> r.name)
|> List.sort_uniq compare
|> List.filter (fun r -> not (List.mem r lexical))
in
let unvisited = List.filter (fun r -> not (Hashtbl.mem visited r)) all in
if unvisited <> [] then
Printf.printf " (밟지 않은 프로덕션: %s)\n" (String.concat " " unvisited);
check "생성이 모든 프로덕션을 밟는다" (unvisited = [])
(* ------------------------------------------------------------------ *)
(* panic, Never, test *)
(* ------------------------------------------------------------------ *)
let () =
let dir = Filename.concat (Filename.get_temp_dir_name ()) "cool_crashtest" in
ignore (Sys.command (Printf.sprintf "mkdir -p %s" (Filename.quote dir)));
let f = Filename.concat dir "m.cool" in
let run src =
write f src;
let st = Session.create ~root:dir ~std:"../std" () in
Session.test st f
in
let src =
"import \"cool.dev/std/test\" as Test\n\n\
pub enum E {\n\
\ A,\n\
\ B,\n\
}\n\n\
pub fn name(e: E) -> String {\n\
\ match e {\n\
\ A => \"a\",\n\
\ B => crash(\"B는 아직\"),\n\
\ }\n\
}\n\n\
test \"통과\" {\n\
\ Test.assert(name(A) == \"a\")\n\
}\n\n\
test \"assert 실패\" {\n\
\ Test.assert(1 == 2)\n\
}\n\n\
test \"crash 실패\" {\n\
\ Test.assert(name(B) == \"b\")\n\
}"
in
match run src with
| Error es ->
List.iter
(fun (e : Session.error) -> Printf.printf " (%s)\n" e.message)
es;
check "crash/test 예제가 검사를 통과한다" false
| Ok rs ->
check "crash/test 예제가 검사를 통과한다" true;
check "테스트 셋이 잡힌다" (List.length rs = 3);
let failed =
List.filter (fun (r : Interp.test_result) -> r.t_failure <> None) rs
in
check "실패는 둘이다" (List.length failed = 2);
(* Never가 match 팔에 서지 못하면 위 예제가 타입 검사를 통과하지 못한다 *)
check "assert 실패가 보고된다"
(List.exists
(fun (r : Interp.test_result) ->
r.t_name = "assert 실패"
&&
match r.t_failure with
| Some (_, m) -> has_sub m "assert 실패"
| None -> false)
rs);
check "crash 메시지가 그대로 보고된다"
(List.exists
(fun (r : Interp.test_result) ->
r.t_name = "crash 실패"
&&
match r.t_failure with
| Some (_, m) -> has_sub m "B는 아직"
| None -> false)
rs);
(* 하나가 죽어도 나머지는 돈다 — 격리는 런타임의 일이다 *)
check "실패해도 다른 테스트는 돈다"
(List.exists
(fun (r : Interp.test_result) ->
r.t_name = "통과" && r.t_failure = None)
rs)
(* 테스트는 effect를 수행할 수 없다 — capability를 받지 않으므로 증명된다 *)
let () =
let dir = Filename.concat (Filename.get_temp_dir_name ()) "cool_crashtest" in
let f = Filename.concat dir "e.cool" in
write f
"pub capability Db {\n\
\ fn read() effects {Db.read} -> Int\n\
}\n\n\
pub fn sneaky() effects {Db.read} -> Int\n\n\
test \"금지\" {\n\
\ let n = sneaky()\n\
}";
let st = Session.create ~root:dir ~std:"../std" () in
Session.load st f;
check "테스트는 effect를 수행할 수 없다"
(List.exists
(fun (e : Session.error) -> has_sub e.message "테스트는 effect를 수행할 수 없습니다")
(Session.errors st))
(* samples/app의 테스트가 실제로 돈다 *)
let () =
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
match Session.test st "../samples/app/config.cool" with
| Error es ->
List.iter
(fun (e : Session.error) -> Printf.printf " (%s)\n" e.message)
es;
check "samples/app의 테스트가 돈다" false
| Ok rs ->
check "samples/app의 테스트가 돈다" (List.length rs >= 4);
check "samples/app의 테스트가 전부 통과한다"
(List.for_all (fun (r : Interp.test_result) -> r.t_failure = None) rs)
(* crash는 scope 트리를 타고 올라간다. 자식이 crash하면 그 scope는 실패하고,
scope 뒤의 코드는 실행되지 않는다.
v0는 태스크가 하나라 이 규칙이 축약된 형태로만 관측된다. 아래에서 "자식
셋"이 안 도는 것은 취소가 아니라 순차 실행의 부산물이다 — 취소는 아직
정하지 않았다. 고정하는 것은 전파이지 형제의 운명이 아니다. *)
let () =
match
run_src
(console
^ "pub fn main(c: Console, root: TaskScope)\n\
\ effects {Console.print, TaskScope.spawn} {\n\
\ c.print(\"scope 앞\")\n\
\ scope work = root {\n\
\ work.spawn(fn() { c.print(\"자식 하나\") })\n\
\ work.spawn(fn() { crash(\"자식 둘이 죽었다\") })\n\
\ }\n\
\ c.print(\"scope 뒤\")\n\
}")
with
| out, Some e ->
check "자식의 crash가 scope 밖으로 전파된다" (has_sub e.message "자식 둘이 죽었다");
check "crash 전의 출력은 남는다" (has_sub out "자식 하나");
check "scope 뒤는 실행되지 않는다" (not (has_sub out "scope 뒤"))
| _, None ->
check "자식의 crash가 scope 밖으로 전파된다" false;
check "crash 전의 출력은 남는다" false;
check "scope 뒤는 실행되지 않는다" false
(* ------------------------------------------------------------------ *)
(* dogfoods — 언어가 실제 문제를 표현하는가 *)
(* *)
(* samples가 검사기를 시험한다면 여기는 언어를 시험한다. 통과가 회귀 *)
(* 테스트가 된다 — 규칙을 바꿨을 때 어느 유스케이스가 깨지는지 보인다. *)
(* *)
(* 기준이 둘인 이유: check만 보면 공허하다. import가 조용히 해소되지 *)
(* 않으면 그 모듈의 이름이 전부 불투명해져 무엇이든 통과한다. *)
(* 실제로 첫 시도에서 그 일이 일어났다 (FINDINGS D1). *)
(* ------------------------------------------------------------------ *)
let () =
let cases =
[
("../dogfoods/std-draft/fs.cool", []);
( "../dogfoods/atomic-update/naive.cool",
[ "../dogfoods/std-draft/fs.cool" ] );
( "../dogfoods/atomic-update/careful.cool",
[ "../dogfoods/std-draft/fs.cool" ] );
]
in
List.iter
(fun (path, deps) ->
let st = Session.create ~root:(Filename.dirname path) ~std:"../std" () in
Session.load st path;
let errs = Session.errors st in
List.iter
(fun (e : Session.error) ->
Printf.printf " (%s)\n" (Session.string_of_error e))
errs;
check (Filename.basename path ^ ": check 통과") (errs = []);
(* 의존 모듈이 실제로 로드됐는가 — 이게 없으면 위 줄이 공허하다.
경로 표기가 여러 가지라(../a/../b) 파일 이름으로 맞춘다 *)
let loaded =
Hashtbl.fold
(fun k _ acc -> Filename.basename k :: acc)
st.Session.modules []
in
List.iter
(fun d ->
check
(Filename.basename path ^ ": " ^ Filename.basename d
^ "이(가) 실제로 해소된다")
(List.mem (Filename.basename d) loaded))
deps)
cases
(* D3: 자원을 들고 ?를 쓰면 정리를 건너뛰는데 검사기는 통과시킨다.
v0 정책("오용 금지, 누수 허용")이고, with가 닫으려는 자리다.
이 테스트는 그 사실을 고정한다 — 나중에 정책이 바뀌면 여기가 깨진다. *)
let () =
let st = Session.create ~root:"../dogfoods/atomic-update" ~std:"../std" () in
Session.load st "../dogfoods/atomic-update/naive.cool";
check "누수는 아직 오류가 아니다 (v0 정책)" (Session.errors st = [])
(* D10 회귀: Option/Result 패턴이 타입을 나른다.
전에는 내장 생성자가 typecheck의 ctors에 없어 None이 변수 바인딩이 되고
Ok(v)의 v가 TUnknown이었다 — 즉 핵심 오류 처리 수단의 패턴 매칭이 타입을
전혀 검사하지 않았다. *)
let () =
check "Ok(v)의 v가 타입을 갖는다"
(type_has
"fn f(r: Result[Int, String]) -> String {\n\
\ match r {\n\
\ Ok(v) => v,\n\
\ Err(e) => e,\n\
\ }\n\
}"
"String이(가) 필요한데 Int");
check "None은 변수 바인딩이 아니다"
(type_has
"fn f(o: Option[Int]) -> Int {\n\
\ match o {\n\
\ None => \"x\",\n\
\ Some(v) => v,\n\
\ }\n\
}"
"match 팔의 타입이 서로 다릅니다")
(* 보안 정리 (ii): safe code에서 capability는 복제되지 않는다.
struct 필드를 통한 구멍이 있었다 — 필드 접근이 빌린 값을 돌려주는데
Move 자리에서 그것을 검사하는 곳이 없었다. 빌린 wrapper에서도 됐다. *)
let () =
let src =
"capability Pay {\n\
\ fn charge(n: Int) effects {Pay.charge}\n\
}\n\n\
struct Wrapper {\n\
\ pay: Pay,\n\
\ label: String,\n\
}\n\n\
fn consume(own p: Pay) effects {Pay.charge}\n\n"
in
check "capability를 필드로 복제할 수 없다"
(move_has
(src
^ "fn duplicate(w: Wrapper) effects {Pay.charge} {\n\
\ consume(w.pay)\n\
\ consume(w.pay)\n\
}")
"부분 move가 없습니다");
check "소유한 struct에서도 필드를 꺼낼 수 없다"
(move_has
(src
^ "fn take(own w: Wrapper) effects {Pay.charge} {\n consume(w.pay)\n}")
"부분 move가 없습니다");
check "copyable 필드는 막지 않는다"
(move_errs (src ^ "fn label_of(w: Wrapper) -> String {\n w.label\n}") = [])
+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