Compare commits
21
Commits
5ac899df68
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1baef87b2e | ||
|
|
6bfdcaa0d6 | ||
|
|
28001d9c1d | ||
|
|
01d090b065 | ||
|
|
450e96d326 | ||
|
|
088e1992a5 | ||
|
|
d85816abae | ||
|
|
410363b230 | ||
|
|
2307bafda2 | ||
|
|
5dc214baee | ||
|
|
df775ac4a6 | ||
|
|
78ef07d2ee | ||
|
|
8ff35c5d9b | ||
|
|
bb19a39e04 | ||
|
|
61b1920909 | ||
|
|
bde940eda3 | ||
|
|
6c0d08b6b0 | ||
|
|
5e70712578 | ||
|
|
a9f6527825 | ||
|
|
7be05be77f | ||
|
|
5831af7760 |
@@ -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
@@ -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
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
# 개밥 먹기 보고 — samples/app을 쓰면서 걸린 것들
|
||||
|
||||
2026-08-30. coollang으로 처음 쓴 "일하는 프로그램" 하나(설정 파서 + 리포트
|
||||
도구, 2모듈 292줄)에서 실제로 걸린 마찰을 적는다.
|
||||
|
||||
v0의 샘플 16개는 전부 검사기를 시험하려고 쓴 것이고, 그래서 "언어가 쓸
|
||||
만한가"에 대해서는 아무것도 말해주지 않았다. 이 문서가 v0가 남기는 마지막
|
||||
데이터이자 v1 설계의 첫 입력이다.
|
||||
|
||||
기록 원칙: **불편은 증거와 함께 적고, 해법은 제안까지만 한다.** 여기서
|
||||
바로 고치면 그것은 개밥 먹기가 아니라 기능 추가가 된다.
|
||||
|
||||
---
|
||||
|
||||
## 현황 한눈에
|
||||
|
||||
| | 항목 | 상태 |
|
||||
|---|---|---|
|
||||
| F1 | 리스트의 n번째를 꺼낼 방법이 없다 | **해결** — `List.first`, `List.nth` |
|
||||
| F2 | ~~`else if`가 없다~~ | **취소** — 관찰자가 틀렸다 |
|
||||
| F3 | `String.concat`이 2항이라 중첩 지옥 | **해결** — `String.join` |
|
||||
| F4 | struct의 한 필드만 바꿀 방법이 없다 | **닫음 — 넣지 않는다** |
|
||||
| F5 | fold에 인덱스가 없다 | **해결** — `List.enumerate` |
|
||||
| F6 | Option/Result에 조작 함수가 없다 | **해결** — `std/option`, `std/result` |
|
||||
| F7 | std와 인터프리터가 어긋날 수 있다 | **해결** — 양방향 테스트 |
|
||||
| F8 | 미사용 지역 변수·파라미터를 안 잡는다 | **닫음 — 넣지 않는다** |
|
||||
|
||||
여덟 항목이 전부 처리됐다. 다섯은 해결, 하나는 취소(관찰자 오류), 둘은
|
||||
"넣지 않는다"로 닫았다.
|
||||
|
||||
**닫은 둘을 "보류"가 아니라 "닫음"으로 적는 이유**: 근거까지 적어두지
|
||||
않으면 다음에 같은 논의를 처음부터 다시 하게 된다. 마음이 바뀔 조건도
|
||||
같이 적었다 — 그 조건이 오면 다시 연다.
|
||||
|
||||
## 잘 된 것부터
|
||||
|
||||
**1. 시그니처가 프로그램의 전부를 말한다.**
|
||||
|
||||
```cool
|
||||
pub fn main(c: Console, f: File, a: Args)
|
||||
effects {Console.print, File.read, Args.all}
|
||||
```
|
||||
|
||||
이 한 줄을 읽으면 이 프로그램이 할 수 있는 일이 끝난다. 네트워크를 쓸 수
|
||||
없고, 다른 파일을 쓸 수 없고, 프로세스를 띄울 수 없다 — 문서가 아니라
|
||||
컴파일러가 보장한다. 300줄을 쓰는 내내 이것이 어색하지 않았다. 오히려
|
||||
`f.read`를 쓰려고 `File`을 인자에 추가하는 순간이 "이 함수가 권한을 하나
|
||||
더 갖는다"는 사실을 자각하게 만들었다.
|
||||
|
||||
**2. `?`가 기대대로 동작했다.**
|
||||
|
||||
```cool
|
||||
let path = first_arg(a.all())?
|
||||
let text = f.read(path)?
|
||||
```
|
||||
|
||||
읽기 좋고, 실패 경로가 보이고, 삼켜지지 않는다.
|
||||
|
||||
**3. 소진성이 실전에서 값을 했다.**
|
||||
|
||||
`Value`에 경우 하나(`List`)를 추가해 봤다:
|
||||
|
||||
```
|
||||
config.cool:54:5: 빠진 경우: List(_)
|
||||
config.cool:62:5: 빠진 경우: List(_)
|
||||
config.cool:196:5: 빠진 경우: List(_)
|
||||
config.cool:204:5: 빠진 경우: List(_)
|
||||
```
|
||||
|
||||
고쳐야 할 자리 넷을 전부, 정확히 짚었다. 이것이 없으면 `show_value`만
|
||||
고치고 `get_int`를 잊는다.
|
||||
|
||||
**4. 모듈 경계가 자연스러웠다.** 파싱(`config`)과 출력(`main`)을 나누는 데
|
||||
마찰이 없었고, `Cfg.Config` 같은 한정 이름이 오히려 읽기 좋았다.
|
||||
|
||||
---
|
||||
|
||||
## 걸린 것 — 심각한 순서로
|
||||
|
||||
### F1. 리스트의 n번째를 꺼낼 방법이 없다 (심각)
|
||||
|
||||
인덱싱 연산자를 뺀 결정 자체는 옳다고 본다. 그런데 `std`에 대안이 없다.
|
||||
"첫 원소"를 꺼내려고 이 코드를 네 번 썼다:
|
||||
|
||||
```cool
|
||||
pub fn first_text(xs: List[String]) -> Option[String] {
|
||||
List.fold(xs, None, fn(acc, x) {
|
||||
match acc {
|
||||
Some(prev) => Some(prev),
|
||||
None => Some(x),
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
`String.split(line, "=")`의 결과에서 앞의 둘을 꺼내는 데는 보조 struct
|
||||
`Pick`까지 만들어야 했다 — 순전히 "몇 번째를 보고 있는가"를 나르려고.
|
||||
**304줄 중 약 40줄이 이 문제 하나 때문에 존재한다.**
|
||||
|
||||
> 제안: `List.first`, `List.nth(xs, i) -> Option[a]`, 그리고 `String.split`
|
||||
> 같은 자리에서 흔한 `List.split_first(xs) -> Option[(a, List[a])]`.
|
||||
> 튜플이 없으므로 마지막 것은 문법 결정을 동반한다.
|
||||
|
||||
### F2. ~~`else if`가 없다~~ — 취소. 관찰자가 틀렸다
|
||||
|
||||
처음 이 문서를 쓸 때 "`else if`가 없어서 3~4단 중첩이 된다"고 적었다.
|
||||
**틀렸다.** `else if`는 처음부터 된다 (`lib/parser.ml:480`이 명시적으로
|
||||
`else` 뒤의 `if`를 처리한다).
|
||||
|
||||
그런데도 `config.cool`을 중첩 `if`로 썼다. 언어가 강제한 것이 아니라
|
||||
내가 확인하지 않고 습관대로 쓴 것이고, 그다음 언어를 탓했다.
|
||||
|
||||
고쳐 쓰니 208줄이 196줄이 됐고 `parse_line`은 4단에서 평평한 5갈래가
|
||||
됐다:
|
||||
|
||||
```cool
|
||||
if String.is_empty(line) {
|
||||
cfg
|
||||
} else if String.starts_with(line, "#") {
|
||||
cfg
|
||||
} else if List.len(parts) != 2 {
|
||||
add_problem(cfg, no, ...)
|
||||
} else if String.is_empty(String.trim(head_or(parts, ""))) {
|
||||
add_problem(cfg, no, "이름이 비어 있습니다")
|
||||
} else {
|
||||
add_entry(cfg, ...)
|
||||
}
|
||||
```
|
||||
|
||||
**개밥 먹기 자체에 대한 교훈이다.** 한 사람이 쓴 300줄에서 나온 불편은
|
||||
언어의 성질일 수도 있고 그 사람의 습관일 수도 있다. 둘을 나누려면 불편을
|
||||
적을 때마다 "언어가 정말 막는가"를 확인해야 한다. 확인 없이 적은 것 하나가
|
||||
"심각" 등급을 달고 v1 설계 입력이 될 뻔했다.
|
||||
|
||||
나머지 항목들은 확인했다 — F1은 `List.first`/`nth`가 std에 실제로 없고,
|
||||
F3은 `String.join`이 실제로 없다.
|
||||
|
||||
### F3. `String.concat`이 2항이라 중첩 지옥이 된다 (심각)
|
||||
|
||||
리포트 한 줄이 이렇게 생겼다:
|
||||
|
||||
```cool
|
||||
c.print(String.concat(" ", String.concat(e.key,
|
||||
String.concat(" = ", String.concat(Cfg.show_value(e.value),
|
||||
String.concat(" (", String.concat(Cfg.type_name(e.value), ")")))))))
|
||||
```
|
||||
|
||||
읽을 수 없다. 이 프로그램에서 가장 나쁜 코드이고, 원인은 명확하다.
|
||||
|
||||
> 제안 두 가지. (a) `String.join(sep, List[String])` — 작고 안전하다.
|
||||
> (b) 문자열 보간 `"${key} = ${value}"` — 훨씬 낫지만 문법과 타입 규칙을
|
||||
> 정해야 하고, "한 개념 한 방식"에서 concat과 겹친다.
|
||||
> 지금 판단으로는 (a)를 먼저 넣고 (b)는 체감 데이터를 더 모은 뒤.
|
||||
|
||||
### F4. struct의 한 필드만 바꿀 방법이 없다 — 닫음. 넣지 않는다
|
||||
|
||||
```cool
|
||||
pub fn add_entry(cfg: Config, e: Entry) -> Config {
|
||||
Config {
|
||||
entries: List.push(cfg.entries, e),
|
||||
problems: cfg.problems, // ← 안 바뀌는데 적어야 한다
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
필드가 둘이라 견딜 만하지만 다섯이면 못 쓴다. `take_at`은 세 필드를 매번
|
||||
전부 나열한다.
|
||||
|
||||
> 처음 제안: `Config { ..cfg, entries: x }`. affine 타입에서는 `cfg`가
|
||||
> move되는 것이므로 소유권 규칙과 충돌하지 않는다고 적었다.
|
||||
|
||||
**결론: 넣지 않는다.** 처음 적을 때 놓친 것이 있다.
|
||||
|
||||
전체 나열이 귀찮은 것은 맞다. 그런데 **그 귀찮음이 값을 한다.** `Config`에
|
||||
필드를 하나 추가하면 지금은 모든 생성 지점이 "필드가 빠졌다"고 컴파일
|
||||
오류를 낸다. 컴파일러가 전부 방문하도록 강제한다.
|
||||
|
||||
`..cfg`를 넣으면 그것이 사라진다. 새 필드가 조용히 base의 값을 이어받고,
|
||||
정말로 손봐야 했던 자리를 지나친다.
|
||||
|
||||
**"오류를 더 빨리 잡는다"가 1번 목표인데 F4는 정확히 그것을 깎는 거래다.**
|
||||
브레비티를 얻고 강제 방문을 잃는다. 그 거래가 옳다는 근거가 지금 없다.
|
||||
소유권 규칙과 충돌하지 않는다는 처음의 관찰은 맞지만, 충돌하지 않는 것과
|
||||
넣을 값이 있는 것은 다른 문제다.
|
||||
|
||||
> 다시 열 조건: 다음 개밥 먹기에서 필드 5개 이상인 struct의 갱신 함수가
|
||||
> 여러 개 나오고, **매번의 전체 나열이 실제로 아무것도 잡지 못했다면**
|
||||
> 그때 넣는다. 반대로 한 번이라도 "필드가 빠졌다"가 진짜 버그를 잡았다면
|
||||
> 이 항목은 영구히 닫힌다.
|
||||
|
||||
### F5. fold에 인덱스가 없다 — 해결 (`List.enumerate`)
|
||||
|
||||
줄 번호를 세려고 struct를 하나 더 만들었다:
|
||||
|
||||
```cool
|
||||
pub copyable struct Numbered {
|
||||
no: Int,
|
||||
cfg: Config,
|
||||
}
|
||||
```
|
||||
|
||||
"인덱스가 필요한 fold"는 드문 요구가 아니다.
|
||||
|
||||
> 처음 제안: `List.fold_indexed`. `List.enumerate`가 더 조합적이지만
|
||||
> 튜플이 없어서 불가능하다고 적었다.
|
||||
|
||||
**"튜플이 없어서 불가능하다"가 틀렸다.** 제네릭 struct 하나면 된다:
|
||||
|
||||
```cool
|
||||
pub copyable struct Indexed[a] {
|
||||
i: Int,
|
||||
value: a,
|
||||
}
|
||||
|
||||
pub fn enumerate[a](xs: List[a]) -> List[Indexed[a]]
|
||||
```
|
||||
|
||||
`fold_indexed`보다 이쪽이 낫다. `enumerate` 하나가 기존 `each`/`map`/
|
||||
`filter`/`fold` 전부와 조합된다. `fold_indexed`를 만들면 `map_indexed`,
|
||||
`each_indexed`가 따라오고 그것이 "한 개념 한 방식"을 깨는 방향이다.
|
||||
|
||||
`samples/app`에서 `Numbered` struct가 사라졌다 (244줄 → 232줄):
|
||||
|
||||
```cool
|
||||
// 전 — 줄 번호를 나르려고 struct를 하나 더 만들었다
|
||||
let start = Numbered { no: 1, cfg: Config { entries: [], problems: [] } }
|
||||
List.fold(lines, start, fn(acc, line) {
|
||||
Numbered { no: acc.no + 1, cfg: parse_line(acc.cfg, acc.no, line) }
|
||||
}).cfg
|
||||
|
||||
// 후
|
||||
let lines = List.enumerate(String.split(text, "\n"))
|
||||
List.fold(lines, empty, fn(cfg, l) { parse_line(cfg, l.i + 1, l.value) })
|
||||
```
|
||||
|
||||
이것이 **관찰자가 틀린 두 번째 사례**다(F2에 이어). 둘 다 "언어가 못
|
||||
한다"고 적었는데 확인해 보니 됐다. 마찰을 적을 때 "정말 막히는가"를
|
||||
확인하는 절차가 없으면 이런 것이 v1 설계 입력으로 들어간다.
|
||||
|
||||
### F6. Option/Result에 조작 함수가 하나도 없다 (중간)
|
||||
|
||||
`map`, `unwrap_or`, `or_else`가 없어서 전부 `match`로 풀었다. `match`가
|
||||
나쁜 것은 아니지만, 세 줄이면 될 것이 여섯 줄이 된다.
|
||||
|
||||
> 제안: `Option.map/unwrap_or`, `Result.map/map_err/unwrap_or`.
|
||||
> 타입 이름이 함수의 이름공간이라는 규칙이 이미 있으므로 문법 결정은 없다.
|
||||
|
||||
### F7. std를 늘릴 때마다 인터프리터를 고쳐야 한다 (구조)
|
||||
|
||||
`std/list.cool`에 `filter`를 적으면 `lib/interp.ml`에도 구현을 넣어야
|
||||
한다. 두 곳이 어긋나면 검사는 통과하고 실행이 죽는다.
|
||||
|
||||
이것은 v0의 구조적 한계이고 v1에서 사라진다(std를 coollang으로 구현).
|
||||
다만 v0 동안은 **std 시그니처와 런타임 구현이 일치하는지 검사하는 테스트**가
|
||||
있어야 한다. 지금은 없다.
|
||||
|
||||
---
|
||||
|
||||
### F8. 미사용 지역 변수와 파라미터를 안 잡는다 — 닫음. 넣지 않는다
|
||||
|
||||
투어용 예제를 쓰다 발견했다. 이 코드가 아무 말 없이 통과한다:
|
||||
|
||||
```cool
|
||||
pub fn f(used: Int, never_used: Int) -> Int {
|
||||
let alive = used + 1
|
||||
let dead = 999
|
||||
alive
|
||||
}
|
||||
```
|
||||
|
||||
**미사용 import는 오류로 막아놓고 미사용 지역 변수는 통과시킨다.** 둘 다
|
||||
"쓰지 않는 선언"인데 한쪽만 잡으니 규칙이 고르지 않아 보인다.
|
||||
|
||||
다만 근거의 성격은 다르다. import를 막은 이유는 재검사 범위를 넓히기
|
||||
때문이었고 — 그건 이 아키텍처의 실제 비용이다 — 미사용 지역 변수에는
|
||||
그런 비용이 없다. 그냥 죽은 코드다.
|
||||
|
||||
그래서 이것은 "고치면 되는 항목"이 아니라 **판단이 필요한 항목**이다.
|
||||
철학 1(오류를 더 빨리 잡는다)에는 부합하지만, 디버깅 중에 한 줄 주석
|
||||
처리했다고 컴파일이 막히는 것은 실제로 성가시다. 그 성가심이 잡아주는
|
||||
버그보다 큰지는 지금 데이터로 알 수 없다.
|
||||
|
||||
**결론: 넣지 않는다.** 그리고 처음에 질문을 잘못 잡았다.
|
||||
|
||||
진짜 질문은 "미사용 지역 변수를 잡을 것인가"가 아니라 **"coollang에 경고
|
||||
등급을 만들 것인가"**다.
|
||||
|
||||
지금 이 언어에는 경고가 없다. 모든 진단이 오류이고 빌드를 멈춘다. 미사용
|
||||
지역 변수를 그 등급에 넣으면 디버깅 중에 한 줄 주석 처리했다고 컴파일이
|
||||
막힌다 — 실제로 성가시다. 그래서 자연스럽게 "경고로 만들자"가 나오는데,
|
||||
그것이 **경고 등급의 첫 입주자**가 된다. 경고 등급은 한 번 생기면 자란다.
|
||||
무시되는 진단이 쌓이는 언어가 된다.
|
||||
|
||||
**경고가 없다는 것은 지금 이 언어의 좋은 성질이고, 죽은 지역 변수 하나
|
||||
때문에 팔 것이 아니다.**
|
||||
|
||||
미사용 import와의 불일치는 감수한다. 근거가 다르다 — import는 재검사
|
||||
범위라는 이 아키텍처의 실제 비용을 만들고, 죽은 지역 변수는 아무 비용도
|
||||
만들지 않는다. 규칙이 고르지 않아 보이는 것과 근거가 없는 것은 다르다.
|
||||
|
||||
> 다시 열 조건: 경고 등급을 다른 이유로 만들게 되는 날. 그때는 이 항목이
|
||||
> 첫 입주자가 아니라 두 번째가 되므로 비용 계산이 달라진다.
|
||||
|
||||
---
|
||||
|
||||
## 후속 (같은 날)
|
||||
|
||||
F1, F3, F6을 std 보강으로 처리하고 `samples/app`을 다시 썼다. 문법은
|
||||
건드리지 않았다 — 표본이 한 사람이 쓴 300줄 하나뿐인데 되돌리기 비싼 축을
|
||||
움직일 수는 없다.
|
||||
|
||||
추가한 것: `List.first`, `List.nth`, `String.join`, 그리고 `std/option.cool`,
|
||||
`std/result.cool` (`map`, `unwrap_or`, `ok_or`, `map_err`, `is_ok`).
|
||||
|
||||
결과:
|
||||
|
||||
| | 전 | 후 |
|
||||
|---|---|---|
|
||||
| config.cool | 196줄 | **148줄** |
|
||||
| main.cool | 96줄 | 96줄 |
|
||||
| 합계 | 292줄 | **244줄** |
|
||||
|
||||
출력은 한 글자도 다르지 않다.
|
||||
|
||||
**F1의 값이 확인됐다.** 48줄이 사라졌고 전부 config.cool에서 나왔다 —
|
||||
`head_or`, `second_or`, `Pick`, `take_at`, `first_text`, `first_entry`가
|
||||
통째로 없어졌다. 예측(40줄쯤)과 실제(48줄)가 맞았다.
|
||||
|
||||
```cool
|
||||
// 전
|
||||
let key = String.trim(head_or(parts, "")) // + Pick struct + take_at 20줄
|
||||
|
||||
// 후
|
||||
let key = String.trim(Option.unwrap_or(List.nth(parts, 0), ""))
|
||||
```
|
||||
|
||||
**F3은 줄 수로는 값이 안 보인다.** main.cool이 96줄 그대로다. 4단 중첩
|
||||
`String.concat`을 4줄짜리 `String.join` 배열로 바꿨으니 줄 수가 같다.
|
||||
그런데 읽기는 확실히 낫다:
|
||||
|
||||
```cool
|
||||
// 전
|
||||
c.print(String.concat(" ", String.concat(e.key,
|
||||
String.concat(" = ", String.concat(Cfg.show_value(e.value),
|
||||
String.concat(" (", String.concat(Cfg.type_name(e.value), ")")))))))
|
||||
|
||||
// 후
|
||||
c.print(String.join("", [
|
||||
" ", e.key, " = ", Cfg.show_value(e.value),
|
||||
" (", Cfg.type_name(e.value), ")",
|
||||
]))
|
||||
```
|
||||
|
||||
**줄 수는 읽기 좋음의 대리 지표일 뿐이고, F3에서 그 대리가 깨진다.**
|
||||
다음 개밥 먹기에서는 줄 수 말고 다른 것을 재야 한다.
|
||||
|
||||
**F7도 처리했다.** `Interp.implemented` 목록과 `std/*.cool`의 선언이
|
||||
서로를 덮는지 테스트가 양방향으로 검사한다. 어긋나면 빌드가 깨진다.
|
||||
|
||||
남은 것 없음. F5는 `List.enumerate`로 해결했고(244줄 → 232줄), F4와 F8은
|
||||
"넣지 않는다"로 닫았다. 둘 다 다시 열 조건을 함께 적었다.
|
||||
|
||||
---
|
||||
|
||||
## 결론
|
||||
|
||||
(아래는 처음 쓸 때의 결론이다. 후속에서 F1·F3·F5·F6·F7이 해결되고 F4·F8이
|
||||
닫혔으므로 지금은 역사적 기록이다.)
|
||||
|
||||
v1로 넘길 때 **F1과 F3이 먼저다.** 둘 다 "언어가 틀렸다"가 아니라
|
||||
"std에 없어서 우회했다"이고, 우회 비용이 코드에 그대로 보인다 — 292줄 중
|
||||
40줄쯤이 F1 하나 때문에 존재한다.
|
||||
|
||||
F2는 취소됐다. 그리고 그것이 이 문서에서 두 번째로 중요한 발견이다:
|
||||
확인 없이 적은 불편 하나가 "심각" 등급을 달고 v1 설계 입력이 될 뻔했다.
|
||||
|
||||
반대로 **되돌리기 비싼 결정들은 하나도 후회되지 않았다.** capability를
|
||||
인자로 나르는 것, effect를 시그니처에 적는 것, 실패를 버릴 수 없는 것,
|
||||
소진적 match — 300줄을 쓰는 동안 이 넷이 거추장스러웠던 순간이 없었고,
|
||||
소진성은 오히려 실수를 잡아줬다.
|
||||
|
||||
v0의 질문은 "되돌리기 비싼 결정이 옳은가"였다. 답은 **그렇다**이고,
|
||||
남은 불편은 전부 되돌리기 싼 것들이다.
|
||||
+205
-83
@@ -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" | "self" |
|
||||
* "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,62 @@ 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 ] , "(" , [ cap_params ] , ")" ,
|
||||
[ NEWLINE ] ,
|
||||
[ eff_result , [ NEWLINE ] ] ,
|
||||
[ "->" , type ] ,
|
||||
[ NEWLINE ] ;
|
||||
|
||||
(* capability 메서드만 첫 자리에 수신자를 적을 수 있다.
|
||||
*
|
||||
* 빌리는 것이 기본이므로 빌림에는 적을 말이 없다 — `self`만 적는 문장은
|
||||
* 존재하지 않는다. 뜻이 있는 쪽에만 표기가 있다.
|
||||
*
|
||||
* 일반 fn에는 이 자리가 없다. 검사기가 거부하는 것이 아니라 문장이
|
||||
* 존재하지 않는다.
|
||||
*
|
||||
* "own"을 먼저 먹고 그 다음을 보므로 선읽기는 여전히 한 토큰이다 *)
|
||||
cap_params = "own" , cap_own_rest
|
||||
| plain_param , list_rest<param> ;
|
||||
cap_own_rest = "self" , list_rest<param>
|
||||
| plain_param , list_rest<param> ;
|
||||
|
||||
const_decl = "const" , ident , ":" , type , "=" , expr ;
|
||||
|
||||
gen_params = "[" , gen_param , { "," , gen_param } , [ "," ] , "]" ;
|
||||
gen_params = "[" , list<gen_param> , "]" ;
|
||||
gen_param = ident , [ ":" , "effects" ] ;
|
||||
(* ident 단독 = 타입 파라미터, ": effects" = effect 파라미터 *)
|
||||
|
||||
params = param , { "," , param } , [ "," ] ;
|
||||
param = [ "own" ] , [ "mut" ] , ident , ":" , type ;
|
||||
(* 무표기 = use(빌림). own만이 소유 이전을 뜻한다.
|
||||
* 수식어 순서 고정: 바인딩 수식어(own, mut)가 먼저, 타입 수식어(affine)는 type 안 *)
|
||||
params = list<param> ;
|
||||
param = [ "own" ] , plain_param ;
|
||||
plain_param = [ "mut" ] , ident , ":" , 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 +185,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 +228,110 @@ 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 리터럴인지 갈리지 않는다. "{"를 보고
|
||||
* 갈리므로 왼쪽으로 인수분해해 적는다 — 파서가 실제로 하는 일이다 *)
|
||||
(* 한정된 이름이 여기 있는 이유는 struct 리터럴 때문이다. Alias.Name { ... }를
|
||||
* postfix의 필드 접근으로 두면 뒤의 "{"를 받을 자리가 없고, 그러면 가져온
|
||||
* 모듈의 struct를 만들 수 없다 — 타입은 쓸 수 있는데 값은 못 만드는 상태다.
|
||||
* "." 를 먼저 먹고 갈라지므로 선읽기는 한 토큰이다 *)
|
||||
(* Alias.Name { ... } — 가져온 모듈의 struct를 만드는 유일한 문법이다.
|
||||
* 이것이 없으면 다른 모듈의 타입을 쓸 수는 있는데 값은 못 만든다.
|
||||
*
|
||||
* 한정된 이름이 struct 자리에만 있고 postfix에는 없는 이유: 뒤에 "{"가
|
||||
* 오는지로 갈리는데, 필드 접근으로 먼저 읽으면 그 "{"를 받을 자리가 없다.
|
||||
* 그래서 struct를 만들 수 있는 문맥(<yes>)에서만 점을 미리 먹는다.
|
||||
* <no>에서는 안 먹으므로 if x.y { } 의 "{"는 언제나 블록이다 *)
|
||||
name_or_struct<yes> = ident , ( "." , ident , [ struct_body ]
|
||||
| [ struct_body ] ) ;
|
||||
name_or_struct<no> = ident ;
|
||||
struct_body = "{" , brace_list<field_init> , "}" ;
|
||||
field_init = ident , ":" , expr ;
|
||||
|
||||
list_lit = "[" , [ args ] , "]" ;
|
||||
(* 빈 리스트는 타입 주석이 필요하다: let xs: List[Int] = [] *)
|
||||
|
||||
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 알고리즘이 처리한다) *)
|
||||
|
||||
+191
-2
@@ -28,6 +28,16 @@ AI의 코드 생성 속도 >> 신뢰 확보 속도.
|
||||
→ "Effects: +payment.refund" 수준으로 리뷰 압축
|
||||
5. 한 개념 = 한 방식: syntax variant 최소화, 공식 formatter 하나,
|
||||
공식 toolchain 하나. 단순성의 기준은 "작성자"가 아니라 "리뷰어와 검증기"
|
||||
6. 이름은 관례가 아니라 뜻에서 고른다 — 낯섦은 한 번 치르고 끝나지만
|
||||
부정확함은 읽는 사람마다 매번 치른다
|
||||
→ 판정: 그 단어로 그것이 무엇인지 평범한 문장을 써 보라. 단어가 문장을
|
||||
도우면 맞는 이름, 문장과 싸우면 틀린 이름이다
|
||||
"크래시는 복구하는 것이 아니라 조사하는 것이다" — 돕는다
|
||||
"패닉은 복구할 수 없다" — 다른 언어에서는 할 수 있어 싸운다
|
||||
→ 널리 쓰인다는 것은 옳다는 증거가 아니다. 운영체제의 "fault"가 그렇다 —
|
||||
잘못이 아닌 것을 잘못이라 부르고, 그 오류까지 함께 전파된다
|
||||
※ 관례가 정확하면 관례를 따른다. 관례라서가 아니라 정확해서다
|
||||
※ 이름에만 적용된다. 동작을 관례와 다르게 만들 이유는 아니다
|
||||
|
||||
■ 파생 결정
|
||||
|
||||
@@ -47,8 +57,14 @@ Alias/Move 모델 (철학 1,3에서 파생 — 언어 전체의 토대):
|
||||
"소비 여부를 타입에 드러내라"는 압력이라 리뷰 가능성에는 순기능이고,
|
||||
실제로 고통스러우면 v1에서 Option::take류 표준 관용구로 흡수한다
|
||||
- v0의 자원·capability는 affine이다: 중복 사용은 compile error,
|
||||
미해제는 검사하지 않는다 (오용 금지, 누수 허용).
|
||||
linear 검사와 해제 보장은 v1 과제
|
||||
미해제는 타입 층에서 검사하지 않는다 (오용 금지, 누수 허용).
|
||||
linear 검사와 해제 보장은 v1 과제.
|
||||
※ 다만 lint가 조기 탈출(?, return) 자리에서 새는 것을 말한다. 막지 않고
|
||||
말한다 — 언어가 강제할지는 with가 결정할 문제이고 그 결정에는 "실제로
|
||||
얼마나 새는가"라는 측정이 필요하다. 저장소 전체에서 네 곳, 오탐 0건
|
||||
- v0에 부분 move는 없다. affine 값이 합성 구조를 드나드는 모든 자리가 Move
|
||||
자리다: struct 필드, 열거형 생성자, 열거형 분해, 리스트·struct 리터럴,
|
||||
함수 인자, 반환, 클로저 capture. 앞의 셋이 뚫려 있었고 개밥 먹기가 찾았다
|
||||
- v0에 first-class reference는 없다. mutable 데이터는 소유 변수를 통해서만 변경
|
||||
- 클로저는 mut 바인딩을 capture할 수 없다. 참조가 없으므로 별칭을 만들 수도,
|
||||
조용히 복사할 수도 없기 때문이다. spawn 클로저 제한은 이 일반 규칙의 특수 사례다
|
||||
@@ -338,6 +354,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 +487,74 @@ 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 self. capability 메서드의 첫 자리에만 오고, 빌리는
|
||||
것이 기본이므로 빌림에는 적을 말이 없다. 정리에 자원 자신만 있으면 되게
|
||||
됐고, 부수로 권한 약화가 공짜로 생겼다: 핸들만 넘기면 그 파일에 쓸 권한만
|
||||
넘어가고 파일 시스템 전체 권한은 안 넘어간다.
|
||||
고침: 함수 타입과 클로저 파라미터의 own. 그 구멍이 std의 fold가 틀린 것을
|
||||
덮고 있었다 — 누적자는 매 단계 소비되므로 own이다.
|
||||
LRU 캐시가 제약 하나를 더했다: 축출은 렉시컬이 아니므로 with가 못 닿는다.
|
||||
따라서 자원은 렉시컬 전용으로 간다 — 자원을 담는 컨테이너(풀, 캐시,
|
||||
레지스트리)는 런타임만 만들 수 있고, 사용자는 언어로 못 만든다. 그 대가를
|
||||
알고 택한다. 이미 그렇게 되어 있었다 — std-draft의 Pool이 불투명한 런타임
|
||||
capability인 이유가 만들 수 없어서였다.
|
||||
그리고 큰 버그 하나를 잡았다: Option/Result의 패턴이 타입을 통째로 잃고
|
||||
있었다. 내장 생성자가 typecheck에 등록되지 않아 None이 변수 바인딩이 되고
|
||||
Ok(v)의 v가 TUnknown이었다. Ok(v) => v 로 Int를 String 자리에 두는 코드가
|
||||
통과했다. 소진성 검사는 자기 목록에 내장을 갖고 있어 이 사실을 덮고 있었다.
|
||||
Mini Shell(1번)이 네 번째이자 다른 모양의 점을 줬다. 앞의 셋은 한 자원이
|
||||
한 곳으로 갔다. 파이프는 끝이 둘이고 각각 다른 자식에게 간다 — 부모에게는
|
||||
아무것도 안 남고 어느 블록에도 묶이지 않는다. 가장 어려운 자원 코드가
|
||||
렉시컬하지 않다.
|
||||
그래서 with를 미룬다. resource라는 새 종류는 넣지 않기로 확정했다 —
|
||||
capability가 이미 그것이고 own self가 정리를 말하므로, 종류를 더 만들면
|
||||
95%가 같은 둘이 생긴다. with는 두 갈래(자동 반납 / 소비 강제) 중 하나를
|
||||
고르는 문제이고, 렉시컬 도구를 먼저 넣으면 그것이 닿지 않는 자리를 나중에
|
||||
다시 설계해야 한다. 다시 여는 조건: Process Supervisor나 Reverse Proxy에서
|
||||
렉시컬한 자원 코드가 다수면 고른다.
|
||||
Mini Shell이 보안 구멍 셋을 더 찾았다. 열거형 생성자가 인자를 소비하지
|
||||
않았고(Some(f)를 두 번), 소유한 열거형을 두 번 분해할 수 있었고(같은
|
||||
파이프에서 같은 fd를 두 번), 점 왼쪽을 이름 해소가 검사하지 않아 가져오지도
|
||||
않은 이름이 조용히 통과했다(Nonsense.whatever). 셋 다 D1·D11과 같은 병이다 —
|
||||
규칙은 있었는데 그 규칙이 닿아야 할 자리 하나가 비어 있었다.
|
||||
그리고 개밥이 처음으로 실제로 돌았다. 파싱을 자원에서 떼어 놓았더니
|
||||
mini-shell/parse.cool의 내장 test 5개가 통과한다.
|
||||
전문은 dogfoods/FINDINGS.md.
|
||||
|
||||
■ 개밥 먹기 1 — samples/app (2026-08)
|
||||
samples/app — 설정 파서 + 리포트 도구, 2모듈 232줄. 검사기를 시험하려고
|
||||
쓴 것이 아니라 일을 하려고 쓴 첫 프로그램이다.
|
||||
결과: 되돌리기 비싼 결정은 하나도 후회되지 않았고(capability 전달, effect
|
||||
명시, 실패를 버릴 수 없음, 소진적 match), 불편은 전부 되돌리기 싼 것들이었다
|
||||
— 리스트 n번째 접근 없음, String.concat 2항. "else if가 없다"고 적은
|
||||
항목 하나는 확인해 보니 관찰자가 틀린 것이었다 — 개밥 먹기의 불편은
|
||||
언어의 성질일 수도, 쓴 사람의 습관일 수도 있다.
|
||||
전문과 증거는 docs/friction.md. 이것이 v1 설계의 첫 입력이다.
|
||||
후속: std 보강(List.first/nth/enumerate, String.join, Option/Result 함수)으로
|
||||
292줄이 232줄이 됐다. 문법은 하나도 건드리지 않았다 — 걸린 것이 전부 std의
|
||||
빈 곳이었지 문법의 문제가 아니었다는 뜻이고, 그것 자체가 결과다.
|
||||
마찰 8건 중 2건은 "언어가 못 한다"고 적었다가 확인해 보니 되는 것이었다
|
||||
(else if, enumerate). 개밥 먹기에는 확인 절차가 함께 있어야 한다.
|
||||
|
||||
■ 측정 (2026-08, v0 fast path)
|
||||
100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현.
|
||||
cold 전체 검사 245ms
|
||||
|
||||
@@ -0,0 +1,836 @@
|
||||
# 개밥 먹기 발견 기록
|
||||
|
||||
`dogfoods/`를 쓰면서 나온 것들. 고친 것과 열어둔 것을 같이 적는다.
|
||||
|
||||
---
|
||||
|
||||
## 2. Atomic File Updater (2026-08-30)
|
||||
|
||||
목적: 동시성을 안 섞고 **자원 모델만** 시험한다. 임시 파일에 쓰고, 디스크에
|
||||
내리고, 원자적으로 바꿔치기한다. 실패 경로가 넷이고 전부 정리가 필요하다.
|
||||
|
||||
산출물: `std-draft/fs.cool` 65줄, `atomic-update/naive.cool` 18줄,
|
||||
`atomic-update/careful.cool` 58줄.
|
||||
|
||||
### D1. 상대 경로 import가 패키지로 오인됐다 — 고침
|
||||
|
||||
`import "../std-draft/fs"`가 조용히 해소되지 않았다. `is_package`가 첫
|
||||
세그먼트에 점이 있는지만 봤는데 `..`이 걸렸다.
|
||||
|
||||
**무서운 것은 버그 자체가 아니라 그 결과다.** import가 해소되지 않으면 그
|
||||
모듈의 이름이 전부 불투명해지고, "모르는 것을 틀렸다고 말하지 않는다"는 원칙에
|
||||
따라 검사기가 무엇이든 통과시킨다. 첫 `coolc check` 결과가 exit 0이었는데
|
||||
**아무 뜻도 없었다.** 없는 메서드를 불러도 통과했다.
|
||||
|
||||
고친 뒤 같은 코드가 정확히 잡힌다:
|
||||
|
||||
```
|
||||
capability Fs.Fs에 this_does_not_exist 메서드가 없습니다
|
||||
```
|
||||
|
||||
→ `dogfoods/README.md`의 합격 기준 ②가 여기서 나왔다.
|
||||
|
||||
### D2. 값 있는 식을 문으로 버릴 수 있었다 — 고침
|
||||
|
||||
```cool
|
||||
fs.remove(path) // Result가 조용히 사라진다
|
||||
1
|
||||
```
|
||||
|
||||
이것이 통과했다. 즉 **`Result`를 버리는 방법이 있었다.** 개밥 먹기 1차
|
||||
보고서와 언어 투어에서 내가 "이 언어에는 실패를 버릴 방법이 없다"고 적었는데
|
||||
틀렸다 — `List.each`에 Result 반환 클로저를 못 넘긴다는 좁은 사실을 언어 전체의
|
||||
성질로 일반화했다.
|
||||
|
||||
이제 꼬리가 아닌 자리의 식이 값을 남기면 오류다:
|
||||
|
||||
```
|
||||
이 식이 남기는 Result[Unit, IoError]이(가) 버려집니다
|
||||
(일부러 버리려면 let _ = 로 적으십시오)
|
||||
```
|
||||
|
||||
부산물이 좋다. 정리 경로에서 오류를 **일부러** 무시하는 관용구가 생겼고,
|
||||
버린다는 사실이 코드에 보인다:
|
||||
|
||||
```cool
|
||||
let _ = fs.close(f) // 정리 중의 실패는 삼킨다 — 원래 오류가 더 중요하다
|
||||
discard(fs, tmp, e)
|
||||
```
|
||||
|
||||
### D3. `?`를 자원과 함께 쓸 수 없다 — 이번 개밥 먹기의 본체
|
||||
|
||||
`naive.cool` 18줄은 이렇게 생겼고 **통과한다**:
|
||||
|
||||
```cool
|
||||
let f = fs.create(tmp)?
|
||||
fs.write(f, contents)? // 실패하면 f가 안 닫히고 tmp가 남는다
|
||||
fs.sync(f)?
|
||||
fs.close(f)?
|
||||
fs.rename(tmp, path)?
|
||||
```
|
||||
|
||||
`?`가 조기 반환하므로 정리를 건너뛴다. v0가 "오용 금지, 누수 허용"이라
|
||||
검사기는 아무 말도 하지 않는다.
|
||||
|
||||
제대로 정리하면 `careful.cool` 58줄이 되고 **5단 중첩 match**가 된다.
|
||||
`?`를 한 번도 못 쓴다. 같은 일에 **3.2배**다.
|
||||
|
||||
이것이 `resource` / `with` 제안이 필요한 이유의 전부다. 그리고 그 제안이
|
||||
암묵적 drop보다 나은 이유도 여기서 보인다 — 정리 지점이 닫는 중괄호로 눈에
|
||||
보이고, `Fs.close`가 effects 절에 나타나 검사된다.
|
||||
|
||||
### D4. capability 메서드가 수신자를 소비할 수 없다 — 고침
|
||||
|
||||
`close`가 핸들을 소비해야 하는데 `own`은 파라미터에만 붙었다. capability
|
||||
메서드에는 수신자를 적는 자리가 없었다.
|
||||
|
||||
처음에는 우회했다 — 핸들의 메서드가 아니라 `Fs`의 메서드로 두고 핸들을
|
||||
인자로 받는 형태다. "읽기에 나쁘지 않고 오히려 권한이 필요하다는 게
|
||||
보인다"고 적었는데, **D9이 그 판단을 뒤집었다.** 정리에 바깥 권한이
|
||||
따라붙으면 `with`가 자원만 보고 무엇을 부를지 알 수 없다.
|
||||
|
||||
**넣은 것**: `own self`를 capability 메서드의 첫 파라미터로.
|
||||
|
||||
```cool
|
||||
pub capability WriteFile {
|
||||
fn write(s: String) effects {WriteFile.write} -> Result[Unit, IoError]
|
||||
fn close(own self) effects {WriteFile.close} -> Result[Unit, IoError]
|
||||
}
|
||||
```
|
||||
|
||||
빌리는 것이 기본이므로 **빌림에는 적을 말이 없다.** `self`만 적는 문장은
|
||||
존재하지 않고, 그것을 쓰면 문법이 아니라 뜻으로 거절한다:
|
||||
|
||||
```
|
||||
self는 own self로만 적습니다 (수신자를 빌리는 것이 기본이라 적을 말이 없습니다)
|
||||
```
|
||||
|
||||
일반 `fn`에는 이 자리가 문법에 없다. 좌인수분해로 `"own"`을 먼저 먹고
|
||||
그다음을 보므로 LL(1)이 유지된다 (진짜 충돌 0건).
|
||||
|
||||
**부수 발견 — 권한이 좁아진 채로 전달된다.** 1판에서는 파일에 쓰게 하려면
|
||||
`Fs`를 통째로 줘야 했다. 이제 핸들만 넘기면 그 파일에 쓸 권한만 넘어간다.
|
||||
정리를 자원 쪽으로 옮긴 것이 권한 약화(attenuation)를 공짜로 만들었다 —
|
||||
설계할 때 노린 것이 아니라 나중에 보인 것이다.
|
||||
|
||||
**부수 발견 — 중첩 수신자에 구멍이 있었다.** move 검사의 `callee_info`가
|
||||
수신자가 이름일 때만 시그니처를 찾았다. `h.lease.release()`는 못 찾아
|
||||
조용히 빌림이 됐고, `own self`가 생기는 순간 그것은 자원을 두 번 소비할 수
|
||||
있다는 뜻이 된다. 같은 커밋에서 막았다 (D11의 부분 move 검사로 떨어진다).
|
||||
|
||||
**부수 발견 — 진단이 부정확했다.** 메서드 이름만으로 아무 capability에서나
|
||||
시그니처를 찾았기 때문에, 이름이 겹치면 엉뚱한 시그니처를 읽고 "빌린 값이라
|
||||
소비할 수 없습니다"를 잘못 찍었다. 수신자의 선언된 타입을 알면 그 capability
|
||||
안에서만 찾도록 고쳤다.
|
||||
|
||||
**2판으로 다시 쓴 결과** (같은 일을 하는 코드):
|
||||
- `dogfoods/std-draft/fs.cool` — `WriteFile`이 메서드를 갖는다. 1판에서는 하나도 없었다
|
||||
- `atomic-update/careful.cool` — `abandon`이 `fs`를 닫는 데 쓰지 않는다
|
||||
- `db-pool/careful.cool` — `release_then(db, pool, l, r)` → `release_then(l, r)`
|
||||
- `lru-cache/resources.cool` — `drain(db, pool, p)` → `drain(p)`
|
||||
|
||||
**도우미의 시그니처가 정리의 진짜 의존을 말하게 됐다.** 그것이 D9이 요구한
|
||||
것이고, `with`의 전제 조건이다.
|
||||
|
||||
### D5. 함수 타입에 `own`이 없다 — 열림, 그리고 이건 구멍이다
|
||||
|
||||
```cool
|
||||
pub fn apply(f: fn(own Handle) -> Handle, own h: Handle) -> Handle
|
||||
^^^ 타입이(가) 필요합니다 — own 발견
|
||||
```
|
||||
|
||||
문법이 `fn_type = "fn" "(" type_list ")"`라 `own`이 못 들어간다. 그래서
|
||||
**소유권을 가져가는 클로저를 타입으로 표현할 수 없다.**
|
||||
|
||||
결과로 이것이 통과한다:
|
||||
|
||||
```cool
|
||||
List.fold(xs, h, fn(acc, n) { step(acc, n) }) // step은 own을 받는다
|
||||
```
|
||||
|
||||
`fold`의 시그니처는 `fn(acc, a) -> acc`이고 무표기는 빌림인데, 클로저
|
||||
리터럴의 파라미터에는 소유권 표시가 없어 move 검사기가 소유한 값처럼
|
||||
취급한다. **타입은 "빌린다"고 말하는데 리터럴은 "가져간다"처럼 행동한다.**
|
||||
|
||||
기본 검사는 멀쩡하다 — 빌린 값 옮기기도 이중 소비도 정확히 잡는다.
|
||||
**고차 경계에서만 뚫린다.** Atomic Updater는 선형 코드라 안 걸렸지만,
|
||||
Mini Shell(파이프라인을 따라 FD를 나름)과 DB Pool(lease를 fold로 다룸)은
|
||||
정면으로 걸린다.
|
||||
|
||||
→ 그 둘을 쓰기 전에 결정해야 한다.
|
||||
|
||||
### D5 후속 — 고침, 그리고 std의 실수 하나가 딸려 나왔다
|
||||
|
||||
`own`을 함수 타입과 클로저 파라미터에 넣었다:
|
||||
|
||||
```cool
|
||||
fn(own Handle) -> Handle // 타입에 적을 수 있다
|
||||
List.fold(xs, h, fn(own acc, n) {...}) // 리터럴에도 적는다
|
||||
```
|
||||
|
||||
클로저 파라미터의 소유권은 **리터럴이 스스로 적는다.** 타입은 기대 타입에서
|
||||
읽어오지만 소유권은 읽어오지 않는다 — move 검사는 타입 검사와 별도 순회라
|
||||
타입을 모르고, 소유권은 타입보다 결과가 크기 때문이다.
|
||||
|
||||
`unify`가 정확히 일치를 요구한다. 빌리는 클로저를 소유 자리에 넘기는 것은
|
||||
안전하지만 그 반대는 아니고, 방향을 다루려면 부분 타입이 필요한데 없다.
|
||||
|
||||
**딸려 나온 것**: `std/list.cool`의 `fold`가 틀려 있었다.
|
||||
|
||||
```cool
|
||||
f: fn(acc, a) -> acc // 전 — 빌림
|
||||
f: fn(own acc, a) -> acc // 후 — 누적자는 매 단계 소비되고 새것으로 바뀐다
|
||||
```
|
||||
|
||||
빌림으로 적혀 있어서 **affine 값을 fold로 실어나를 수 없었다.** 그런데 그
|
||||
사실이 드러나지 않았던 이유가 바로 이 구멍이었다 — 클로저 파라미터를 무조건
|
||||
소유로 봤으니 아무 오류도 안 났다. **구멍이 자기가 숨긴 버그를 덮고 있었다.**
|
||||
|
||||
#### 남은 한계 — 제네릭을 통과해 보지 못한다
|
||||
|
||||
move 검사는 타입이 없어 `fold`의 `acc`가 호출 지점에서 무엇으로 묶이는지
|
||||
모른다. 그래서 클로저 파라미터에 표기가 없고 기대 타입이 제네릭 변수면
|
||||
affinity를 판정하지 못한다. 지금은 소유권 표기 불일치로 잡히지만, 표기가
|
||||
양쪽 다 없으면 통과한다.
|
||||
|
||||
근본 해법은 move 검사가 타입을 보는 것이고, 그건 두 순회를 합치는 일이다.
|
||||
v0에서는 하지 않는다.
|
||||
|
||||
#### 대가 하나 — own이 흔해진다
|
||||
|
||||
`own`은 흔하지 않은 쪽에 붙는 표기인데, `fold`가 항상 요구하면 흔해진다.
|
||||
`Config` 같은 copyable 누적자에도 `own`을 적게 된다. 정확히 일치를 요구한
|
||||
결과이고, 부분 타입을 넣으면 사라진다. **표기의 신호가 약해지는지 지켜본다.**
|
||||
|
||||
### D6. 문자에 접근할 방법이 없다 — 열림
|
||||
|
||||
`String`에 `split`, `trim`, `starts_with`, `contains`뿐이다. 인덱싱도
|
||||
`chars`도 없어 어휘 분석을 쓸 수 없다. Atomic Updater에는 파싱이 없어 안
|
||||
걸렸지만 Mini Shell은 여기서 막힌다.
|
||||
|
||||
되돌리기 싼 std 문제다.
|
||||
|
||||
### D7. effect-free 테스트로는 이 프로그램을 하나도 테스트할 수 없다 — 예상됨
|
||||
|
||||
`update`는 전부 effect다. 테스트는 capability를 받지 않으므로 부를 수 없다.
|
||||
설계대로이고, 대가가 이제 실물로 보인다.
|
||||
|
||||
가짜 capability를 만들 수단이 없는 한 이 층은 테스트 밖에 있다.
|
||||
※ 가짜 capability를 허용해도 보안 성질은 안 깨질 것으로 보인다 — 클로저로
|
||||
만든 가짜 `Fs`는 진짜 권한을 갖지 않는다. 실제로 불편해진 뒤에 판단한다.
|
||||
|
||||
### 소유권 검사가 실제로 잡는 것 (확인)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 두 번 닫기 | **잡힘** — `f은(는) 이미 move되었습니다 (4:22에서 소비)` |
|
||||
| 닫은 뒤 쓰기 | **잡힘** — 같은 진단 |
|
||||
| 빌린 핸들을 반환 | **잡힘** — `빌린 값이라 반환할 수 없습니다` |
|
||||
| 소유한 핸들을 반환 | 통과 — 옳다. `create`가 그렇게 생겼다 |
|
||||
| 조기 반환으로 누수 | **통과** — v0 정책("오용 금지, 누수 허용") |
|
||||
|
||||
마지막 줄이 D3이고, `with`가 닫으려는 자리다.
|
||||
|
||||
---
|
||||
|
||||
## 6. DB Connection Pool (2026-08-30)
|
||||
|
||||
목적: 자원 모델의 **두 번째 데이터 점**. 파일에 없던 셋을 압박한다 —
|
||||
자원이 두 층(풀/lease), 반납이 파괴가 아님, 획득이 실패할 수 있음.
|
||||
|
||||
산출물: `std-draft/db.cool` 50줄, `db-pool/naive.cool` 25줄,
|
||||
`db-pool/careful.cool` 58줄.
|
||||
|
||||
### D8. 인자 안에서 소비와 사용을 섞을 수 없다
|
||||
|
||||
```cool
|
||||
release_then(db, pool, l, db.query(l, sql))
|
||||
^ ^
|
||||
소비 사용
|
||||
```
|
||||
```
|
||||
l은(는) 이미 move되었습니다 (13:41에서 소비)
|
||||
```
|
||||
|
||||
move 검사가 인자를 왼쪽부터 걷기 때문이다. **실제 평가 순서와 무관하다** —
|
||||
사람은 "질의가 먼저 돌고 그다음 반납"이라고 읽지만 검사기는 그렇게 안 본다.
|
||||
|
||||
우회는 쉽다. 먼저 `let`으로 묶고 넘긴다. 다만 **왜 안 되는지가 코드에서
|
||||
안 보이므로** 진단 메시지가 위치만 말하고 이유를 말하지 않는다.
|
||||
|
||||
### 두 사례 비교 — 이것이 `with` 결정의 근거다
|
||||
|
||||
| | naive | careful | 배수 | match 중첩 | 정리 도우미 |
|
||||
|---|---|---|---|---|---|
|
||||
| Atomic Updater | 18 | 58 | **3.2** | **4단** | 2개 |
|
||||
| DB Pool | 25 | 58 | **2.3** | **1단** | 2개 |
|
||||
|
||||
**공통 (2/2)**
|
||||
|
||||
1. **자원을 들면 `?`를 한 번도 못 쓴다.** 예외 없이 그렇다
|
||||
2. **"정리하고 결과를 그대로 실어나르는 도우미"를 양쪽이 각자 발명했다**
|
||||
(`abandon`/`discard`, `release_then`/`close_then`). 같은 패턴이 두 번
|
||||
독립적으로 나온 것은 그것이 진짜 추상이라는 뜻이다
|
||||
3. **누수는 조용히 통과한다** — 검사기가 아무 말도 안 한다
|
||||
|
||||
**다른 점 — 그리고 이게 중요하다**
|
||||
|
||||
파일은 4단 중첩이고 풀은 1단이다. 이유는 **정리가 단계마다 다른가**이다.
|
||||
|
||||
- 파일: 닫기 전이면 `close + remove`, 닫은 뒤면 `remove`만 → 단계마다 다름
|
||||
- 풀: 성공이든 실패든 언제나 `release` → 하나로 접힘
|
||||
|
||||
즉 **고통이 자원 개수가 아니라 정리의 균일성에 비례한다.** `with`의 값도
|
||||
그만큼 달라진다.
|
||||
|
||||
### D9. `with`가 성립하려면 D4를 먼저 풀어야 한다
|
||||
|
||||
풀 사례가 `with`의 설계 제약 하나를 드러냈다.
|
||||
|
||||
```cool
|
||||
with l = db.acquire(pool)? { ... } // 나갈 때 무엇을 부르나?
|
||||
```
|
||||
|
||||
반납은 `db.release(pool, l)`이다 — **풀과 권한이 필요하다.** 그런데 `with`가
|
||||
자동으로 부르려면 **자원만으로 정리를 표현할 수 있어야 한다.** 즉:
|
||||
|
||||
```cool
|
||||
resource Lease {
|
||||
release close(own self) effects {Db.release} -> Result[Unit, DbError]
|
||||
}
|
||||
```
|
||||
|
||||
`own self`가 필요하다. 그런데 **capability 메서드는 수신자를 소비할 수
|
||||
없다(D4)**. `own`은 파라미터에만 붙는다.
|
||||
|
||||
파일 쪽도 같다 — `Fs.close(own f)`는 `Fs` 권한을 요구하므로 자원만으로는
|
||||
정리가 안 된다.
|
||||
|
||||
**따라서 순서가 정해진다: D4 → resource/with.** 자원이 자기 정리를 스스로
|
||||
선언할 수 있어야 `with`가 성립한다. 런타임이 lease 안에 풀을 넣어 두면
|
||||
되므로 구현상의 문제는 아니지만, **언어에 표현할 자리가 없다.**
|
||||
|
||||
→ D4를 고쳤다. `Lease.release(own self)`에는 풀도 `Db`도 없다. 전제가
|
||||
충족됐다.
|
||||
|
||||
### 잘 된 것 — 설계가 버틴 부분
|
||||
|
||||
- **두 층 자원이 자연스럽게 중첩된다.** 풀이 바깥, lease가 안쪽
|
||||
- **반납 ≠ 파괴가 시그니처로 표현된다.** `release(p: Pool, own l: Lease)`
|
||||
- **획득 실패가 `Result`로 자연스럽다.** `Exhausted`가 호출자에게 선택을 준다
|
||||
- `List[String]`의 `List`는 내장 타입이라 import가 필요 없다. 모듈 `List`를
|
||||
가져오는 것은 그 타입의 함수를 쓸 때뿐이다 — 미사용 import lint가 정확히
|
||||
이것을 지적했다
|
||||
|
||||
---
|
||||
|
||||
## LRU 캐시 (2026-08-30)
|
||||
|
||||
목적: 앞의 둘이 못 건드린 데를 친다. 파일과 풀은 정리 지점이 **렉시컬**이었다.
|
||||
LRU는 아니다 — **축출은 삽입의 부작용으로 예측할 수 없는 때에 일어난다.**
|
||||
|
||||
산출물: `lru-cache/values.cool` 82줄, `lru-cache/resources.cool` 73줄.
|
||||
|
||||
### D10. Option/Result 패턴이 타입을 통째로 잃고 있었다 — 고침. 큰 것이었다
|
||||
|
||||
```cool
|
||||
pub fn probe3(r: Result[Int, String]) -> String {
|
||||
match r {
|
||||
Ok(v) => v, // Int를 String 자리에 둔다
|
||||
Err(e) => e,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**통과했다.** 원인: typecheck의 `env.ctors`에 내장 생성자가 없었다. 그래서
|
||||
`None`은 생성자가 아니라 **`None`이라는 이름의 변수 바인딩**이 되고,
|
||||
`Ok(v)`의 `v`는 `TUnknown`이 되어 무엇과도 맞았다.
|
||||
|
||||
즉 **이 언어의 핵심 오류 처리 수단인 Option/Result의 패턴 매칭이 타입을 전혀
|
||||
검사하지 않고 있었다.** 소진성 검사는 자기 `is_ctor`에 내장을 갖고 있어서
|
||||
이 사실을 덮고 있었다 — 빠진 경우는 잡으면서 타입은 안 봤다.
|
||||
|
||||
내장 열거형을 등록해서 고쳤다. 기존 코드는 하나도 안 깨졌다.
|
||||
|
||||
이 버그가 여태 안 보인 이유가 씁쓸하다. `match r { Ok(v) => Int.show(v) }`
|
||||
같은 코드는 v가 TUnknown이어도 통과하므로 **아무도 이상함을 못 느낀다.**
|
||||
틀린 코드를 써봐야 드러난다.
|
||||
|
||||
### D11. struct 필드와 affine 값 — 그리고 여기서 보안 구멍이 나왔다
|
||||
|
||||
처음 이렇게 적었다: "필드 접근은 빌림이라 affine 값을 꺼낼 수 없다."
|
||||
**규칙으로는 맞았는데 구현이 그것을 강제하지 않고 있었다.**
|
||||
|
||||
나중에 "다 고쳤나"를 확인하려고 전부 다시 돌려보다 드러났다:
|
||||
|
||||
```cool
|
||||
pub capability Pay { fn charge(n: Int) effects {Pay.charge} }
|
||||
pub struct Wrapper { pay: Pay }
|
||||
pub fn consume(own p: Pay) effects {Pay.charge}
|
||||
|
||||
pub fn duplicate(w: Wrapper) effects {Pay.charge} {
|
||||
consume(w.pay)
|
||||
consume(w.pay) // 같은 capability를 두 번 소비한다
|
||||
}
|
||||
```
|
||||
|
||||
**통과했다.** 그것도 `w`가 **빌린 값**인데도. 즉 **보안 정리 (ii)("safe
|
||||
code에서 capability는 복제·위조되지 않는다")가 깨져 있었다.** 이 세션에서
|
||||
찾은 것 중 가장 심각하다.
|
||||
|
||||
원인: `E_field`가 빌린 값(`v_use = true`)을 돌려주는데, **`Move` 문맥에서
|
||||
그것을 검사하는 곳이 `E_ident` 분기에만 있었다.** 필드 접근은 그 분기를
|
||||
지나가지 않는다.
|
||||
|
||||
고쳤다. `E_field`가 Move 자리에 놓이고 필드가 affine이면 오류다:
|
||||
|
||||
```
|
||||
pay 필드는 affine이라 다른 함수에 넘길 수 없습니다
|
||||
(v0에는 부분 move가 없습니다 — 꺼내려면 열거형으로 감싸십시오)
|
||||
```
|
||||
|
||||
필드의 affinity를 알려면 바인딩의 선언 타입이 필요해서, move 검사기에
|
||||
struct 필드 표와 바인딩 타입을 넣었다. **copyable 필드는 막지 않는다** —
|
||||
`w.label`은 통과한다.
|
||||
|
||||
그리고 이것이 LRU에서 열거형으로 우회한 것을 사후에 정당화한다. 그때는
|
||||
"struct로는 안 되고 열거형으로는 된다"가 우연처럼 보였는데, **열거형이
|
||||
유일한 길인 것이 규칙이었고 struct 쪽이 새고 있었을 뿐이다.**
|
||||
|
||||
#### 이 버그가 여태 안 보인 이유
|
||||
|
||||
`samples/05_move_errors.cool`은 자원 타입을 **직접** 다룬다. struct에 넣고
|
||||
필드로 꺼내는 코드가 없었다. 개밥 먹기에서 **자원을 자료구조에 담는** 코드를
|
||||
처음 쓰면서 드러났다.
|
||||
|
||||
#### (원래 기록) 열거형으로만 둘을 함께 돌려줄 수 있다
|
||||
|
||||
`put`은 캐시와 축출된 자원을 **함께** 돌려줘야 한다. 튜플이 없으니 struct다.
|
||||
|
||||
```cool
|
||||
pub struct Put { cache: LeaseCache, evicted: Option[Db.Lease] }
|
||||
...
|
||||
match p.evicted { Some(l) => release(l), ... }
|
||||
```
|
||||
```
|
||||
l은(는) 빌린 값이라 다른 함수에 넘길 수 없습니다
|
||||
```
|
||||
|
||||
**필드 접근은 빌림이고, struct를 분해하는 패턴이 언어에 없다.** 그래서 struct에
|
||||
넣은 affine 값은 다시 꺼낼 수 없다.
|
||||
|
||||
열거형은 된다 — 패턴이 분해하기 때문이다:
|
||||
|
||||
```cool
|
||||
pub enum Put {
|
||||
Kept(LeaseCache),
|
||||
Evicted(LeaseCache, Db.Lease),
|
||||
}
|
||||
match p { Evicted(c, l) => release_then(db, pool, l, c), ... }
|
||||
```
|
||||
|
||||
**즉 "둘을 함께 돌려주기"가 열거형으로만 가능하다.** 튜플이 없는 대가가
|
||||
여기서 두 번째로 나온다(F5에 이어). 그리고 이건 취향 문제가 아니라
|
||||
**표현 가능성 문제**다.
|
||||
|
||||
→ 해법 후보: struct 분해 패턴 + 부분 이동, 또는 튜플. 둘 다 문법 결정이다.
|
||||
|
||||
### D12. 축출은 렉시컬이 아니다 — `with`가 못 닿는 첫 자리
|
||||
|
||||
`with`는 렉시컬 수명만 다룬다. 축출된 lease는 **어느 블록에도 묶이지 않는다.**
|
||||
삽입할 때 튀어나오고, 그 시점은 캐시 상태에 달려 있다.
|
||||
|
||||
그래서 `resource`가 렉시컬 전용이면 **자원을 담는 컨테이너를 언어로 만들 수
|
||||
없다.** 그리고 이것이 이미 우리 설계에 나타나 있었다 — `Pool` 자체가 자원
|
||||
캐시인데, `std-draft/db.cool`에서 **런타임이 주는 불투명한 capability**로
|
||||
선언했다. 만들 수 없어서 그렇게 한 것이다.
|
||||
|
||||
두 갈래다:
|
||||
|
||||
- **(a) 자원은 렉시컬 전용** → 자원 컨테이너는 런타임만 만들 수 있다.
|
||||
풀, 캐시, 레지스트리가 전부 언어 밖이 된다. 단순하고, 지금 상태가 그렇다
|
||||
- **(b) 자원이 자료구조로 탈출할 수 있다** → `with`만으로 부족하고 결국
|
||||
선형 타입이 필요하다. v0가 미룬 바로 그것
|
||||
|
||||
**(a)를 권한다.** 자원 컨테이너는 드물고, 만드는 쪽은 런타임이며, 쓰는 쪽은
|
||||
lease를 렉시컬하게 빌린다. 다만 **이 선택이 무엇을 포기하는지 적어둬야
|
||||
한다** — 사용자가 자기 자원 풀을 언어로 못 만든다.
|
||||
|
||||
### 자료구조 자체의 마찰 (값 판)
|
||||
|
||||
- **Map이 없어 O(n)이다.** LRU의 요점이 O(1)인데 표현할 수단이 없다
|
||||
- **튜플이 없어 운반용 struct를 세 번 만들었다** — `Got`, `Put`, `Dropping`
|
||||
- **`get`이 캐시를 새로 돌려줘야 한다.** 최근성이 바뀌므로. 공유 가변 상태가
|
||||
없다는 것의 대가이고, 호출자가 캐시를 계속 실어날라야 한다
|
||||
- **뒤에서 자르는 함수가 std에 없다.** `drop_last`를 `reverse` 두 번으로 썼다
|
||||
- **fold에 "첫 원소 건너뛰기"가 없어** `Dropping` struct를 또 만들었다
|
||||
|
||||
---
|
||||
|
||||
## 결정 — resource / with
|
||||
|
||||
**넣는다. 다만 D4가 먼저다.**
|
||||
|
||||
근거는 위 표다. 데이터 두 점에서 공통으로:
|
||||
- `?`를 자원과 함께 쓸 수 없다 (2/2)
|
||||
- 같은 정리 패턴을 각자 발명했다 (2/2)
|
||||
- 누수가 조용히 통과한다 (2/2)
|
||||
- 2.3 ~ 3.2배
|
||||
|
||||
그리고 `with`가 암묵적 drop보다 나은 이유가 여기서도 확인된다 — 정리 지점이
|
||||
닫는 중괄호로 보이고, 정리의 effect가 effects 절에 나타나 검사된다.
|
||||
|
||||
## Mini Shell (2026-08-30)
|
||||
|
||||
목적: 앞의 셋이 못 건드린 모양을 친다.
|
||||
|
||||
| | 자원 개수 | 정리 지점 | 한 자원이 |
|
||||
|---|---|---|---|
|
||||
| 파일 | 하나 | 렉시컬 | 한 곳으로 |
|
||||
| 풀 | 두 층 | 렉시컬 | 한 곳으로 |
|
||||
| LRU | 한 종류, 여럿 | **비렉시컬** | 한 곳으로 |
|
||||
| **셸** | **여러 종류, 여럿** | 비렉시컬 | **둘로 갈라져 서로 다른 곳으로** |
|
||||
|
||||
파이프 하나는 끝이 둘이고 각각 다른 자식에게 간다. 부모는 넘긴 뒤 아무것도
|
||||
안 남는다. **어느 블록에도 묶이지 않는다.**
|
||||
|
||||
산출물: `std-draft/os.cool` 95줄, `mini-shell/parse.cool` 130줄,
|
||||
`mini-shell/shell.cool` 155줄.
|
||||
|
||||
**처음으로 개밥이 실제로 돈다.** 파싱을 자원에서 떼어 놓았기 때문이다 —
|
||||
`parse.cool`의 내장 test 5개가 `coolc test`로 통과한다. capability를 받지
|
||||
않으므로 effect-free임이 검사로 증명되고, 그래서 순서에 의존하지 않는다.
|
||||
|
||||
### D13. 열거형 생성자가 인자를 소비하지 않았다 — 고침 (보안)
|
||||
|
||||
```cool
|
||||
fn dup(own f: Fd) -> Option[Fd] {
|
||||
let a = Some(f)
|
||||
Some(f) // 통과했다
|
||||
}
|
||||
```
|
||||
|
||||
리스트 리터럴(`[f, f]`)과 struct 리터럴은 처음부터 Move 자리였는데
|
||||
**생성자만 일반 호출로 취급돼 빠져 있었다.** 사용자 열거형도 내장
|
||||
`Some`/`Ok`도 마찬가지였다.
|
||||
|
||||
**LRU가 affine 값을 열거형으로 실어나르는 우회를 쓰므로, 이 자리가
|
||||
뚫려 있으면 그 우회 자체가 무의미했다.** 넣는 쪽이 검사되지 않으면
|
||||
꺼내는 쪽을 아무리 조여도 소용이 없다.
|
||||
|
||||
### D14. 소유한 열거형을 두 번 분해할 수 있었다 — 고침 (보안)
|
||||
|
||||
```cool
|
||||
fn dup(own p: Pipe) effects {Fd.close} {
|
||||
match p { Ends(a, b) => { take(a) take(b) }, }
|
||||
match p { Ends(a, b) => { take(a) take(b) }, } // 통과했다
|
||||
}
|
||||
```
|
||||
|
||||
`match`가 scrutinee를 항상 빌렸다. 조각은 owned로 꺼내 놓고 원본은
|
||||
소비하지 않았으므로 **같은 파이프에서 같은 fd를 두 번 꺼낼 수 있었다.**
|
||||
|
||||
이름을 묶는 패턴으로 소유한 affine 값을 분해하면 그 값이 소비되게 했다.
|
||||
이름을 묶지 않는 패턴(`Ends(_, _)`)은 아무것도 꺼내지 않으므로 그대로 둔다.
|
||||
|
||||
**D11·D13과 같은 하나의 규칙이 세 자리에서 새고 있었다**: v0에 부분 move가
|
||||
없으므로 affine 값이 합성 구조를 드나드는 모든 자리는 Move 자리여야 한다.
|
||||
struct 필드(D11), 열거형 생성자(D13), 열거형 분해(D14) — 셋 다 뚫려 있었다.
|
||||
남은 자리를 세어 보면 리스트 리터럴, struct 리터럴, 함수 인자, 반환,
|
||||
클로저 capture이고 이들은 처음부터 막혀 있었다.
|
||||
|
||||
### D15. 점 왼쪽을 검사하지 않았다 — 고침 (D1과 같은 병)
|
||||
|
||||
```cool
|
||||
fn f() -> Bool {
|
||||
Nonsense.whatever(1) // 통과했다
|
||||
}
|
||||
```
|
||||
|
||||
`Nonsense`는 어디에도 없다. 그런데 "모르는 것을 틀렸다고 말하지 않는다"는
|
||||
규칙이 **가져오지도 않은 이름에까지 적용되고 있었다.** 그 규칙은 해소할 수
|
||||
없는 패키지 import를 위한 것이지, 아무 이름이나 써도 된다는 뜻이 아니다.
|
||||
|
||||
내장 타입 이름도 모듈이 아니다. 타입으로 쓰는 데는 import가 필요 없지만
|
||||
그 타입에 딸린 함수를 쓰려면 가져와야 한다 — prelude가 없다는 규칙에
|
||||
예외를 두지 않는다.
|
||||
|
||||
**samples 셋이 이 구멍에 기대고 있었다** (02, 04, 06). `Code.show`,
|
||||
`File.close`가 어디에도 정의되지 않은 채 통과하고 있었다. 셋 다 고쳤고,
|
||||
06은 `own self` 덕에 `File.close(f)`가 `f.close()`가 됐다.
|
||||
|
||||
### D16. variant 이름이 모듈 전역이다 — 열림
|
||||
|
||||
```cool
|
||||
pub enum Stdin { Inherit, FromPipe(ReadFd) }
|
||||
pub enum Stdout { Inherit, ToPipe(WriteFd) } // variant Inherit이 중복입니다
|
||||
```
|
||||
|
||||
`InheritIn` / `InheritOut`이 됐다. **뜻이 아니라 언어의 제약이 고른
|
||||
이름이다** — 철학 6이 지지 않아야 할 방식으로 진 자리다.
|
||||
|
||||
원인은 `match` 팔에 열거형 이름을 적지 않는다는 결정이다 (한 개념 한 방식).
|
||||
그 결정 자체는 옳아 보이는데 대가가 여기서 나왔다. 고치려면 `Stdin.Inherit`
|
||||
같은 한정 표기를 식과 패턴 양쪽에 넣어야 하고, 그러면 "한 방식"이 둘이 된다.
|
||||
|
||||
**미룬다.** 두 열거형이 같은 뜻의 variant를 갖는 일은 흔하지만, 지금은
|
||||
데이터 점이 하나뿐이다. Reverse Proxy나 TCP Server에서 또 나오면 그때
|
||||
결정한다.
|
||||
|
||||
### D17. 리스트에 넣은 자원을 꺼낼 수 없다 — 열림, 설계 결정 대기
|
||||
|
||||
셸의 자연스러운 모양은 "자식을 전부 띄우고 리스트에 모아 두었다가 한꺼번에
|
||||
거두기"다. 파이프 버퍼가 차면 막히므로 **띄우기를 다 끝내기 전에 거둘 수
|
||||
없다.**
|
||||
|
||||
넣는 것은 된다 — `[c1, c2]`는 통과하고 `List[Child]`는 전이적으로 affine이다.
|
||||
**꺼내는 것이 안 된다.** `List.fold`의 시그니처가 `fn(own acc, a)`라 원소 `a`는
|
||||
빌린 값이고, 빌린 자원은 소비할 수 없다.
|
||||
|
||||
그래서 재귀가 됐다 — "뒤를 먼저 다 띄우고, 돌아오면서 자기 자식을 거둔다".
|
||||
읽기에 나쁘지 않고 오히려 파이프라인의 모양과 닮았다. 하지만 이것은 우회지
|
||||
답이 아니다.
|
||||
|
||||
이것이 **D12(축출은 비렉시컬)의 반대편**이다. D12는 자원이 컨테이너에서
|
||||
예측 못 할 때 나가는 문제였고, 여기는 아예 나갈 방법이 없는 문제다. 둘이
|
||||
같은 결정을 기다린다: **자원을 담는 컨테이너를 사용자가 만들 수 있는가.**
|
||||
|
||||
### D18. `spawn`이 자원을 가져갔는지 시그니처로 알 수 없다 — 열림
|
||||
|
||||
```cool
|
||||
fn spawn(program: String, args: List[String],
|
||||
own stdin: Stdin, own stdout: Stdout)
|
||||
effects {Os.spawn} -> Result[Child, OsError]
|
||||
```
|
||||
|
||||
`own`이 있으므로 부르는 쪽에서는 소비된다. 그런데 **실패했을 때 안에서
|
||||
정리됐는지는 시그니처가 말하지 않는다.** 실제 `posix_spawn`은 실패해도 fd를
|
||||
닫지 않는다 — 즉 이 시그니처는 진실을 감춘다.
|
||||
|
||||
정직하게 쓰면 `Result[Child, SpawnFailed]`이고 `SpawnFailed`가 fd를 돌려줘야
|
||||
한다. 그러면 실패 처리가 매번 두 겹이 된다.
|
||||
|
||||
**이것은 언어의 결함이 아니라 계약의 문제다.** 다만 언어가 `own`을 강제하는
|
||||
순간 이 질문이 모든 자원 API에서 나온다는 것을 기록해 둔다 — Rust도
|
||||
`std::process`에서 같은 선택을 했고 fd를 돌려주지 않는다.
|
||||
|
||||
### 잘 된 것
|
||||
|
||||
- **파싱과 자원을 나눈 것이 옳았다.** 파싱이 순수하므로 내장 test로 그 자리에서
|
||||
증명된다. 자원이 섞였으면 개밥 먹기 전체가 "쓰기만 한 것"으로 남았다
|
||||
- **`?`가 파싱에서는 필요 없었다.** fold의 누적자가 `Result`라 실패가 그대로
|
||||
실려간다 — 조기 탈출 없이도 뒤 조각이 앞의 실패를 덮지 않는다
|
||||
- **읽는 끝과 쓰는 끝을 타입으로 나눈 것**이 값싸고 정확했다. 읽는 끝에 쓰는
|
||||
것은 프로그램의 결함이고, 결함은 타입이 막는 편이 낫다
|
||||
- **`own self` 뒤로 `child.wait()`가 자연스럽다.** 1판 문법이었다면
|
||||
`os.wait(child)`였고 `Os` 권한을 자식 거두기까지 들고 다녀야 했다
|
||||
|
||||
## Process Supervisor (2026-08-30)
|
||||
|
||||
목적: 앞의 넷은 자원 모델이었다. 여기는 **실패 위상**이다 — 자식이 죽었을
|
||||
때 그 사실이 어디까지 가는가, 누가 멈출 수 있는가.
|
||||
|
||||
산출물: `std-draft/sup.cool` 60줄, `supervisor/workers.cool` 130줄.
|
||||
|
||||
### 확인된 것 — 문서의 주장 셋이 코드가 됐다
|
||||
|
||||
**① spawn은 하나의 개념이고 실패가 어디까지 가느냐가 권한으로 갈린다.**
|
||||
`sc.spawn(f)`는 실패가 부모로 가고 `sup.supervise(sc, f, policy)`는 여기서
|
||||
멈춘다. 실패를 삼키려면 `Supervisor`를 받았어야 하고 **그것이 시그니처에
|
||||
보인다.** 문법 변경 없이 성립한다.
|
||||
|
||||
**② 재시작 가능한 자식은 copyable 클로저여야 한다 — affinity가 검사한다.**
|
||||
이것이 이번 개밥의 가장 값싼 확인이다. `supervise`의 body가 무표기 `fn`이므로:
|
||||
|
||||
```
|
||||
affine 값을 capture한 클로저는 fn 자리에 넘길 수 없습니다 (affine fn이어야 합니다)
|
||||
```
|
||||
|
||||
lease를 들고 가는 워커는 `supervise`에 못 간다. **한 번밖에 못 도는 것을
|
||||
재시작할 수는 없다** — 얼랭이 관례로 지키는 것을 타입이 막는다. 한 번만
|
||||
돌리는 `once`는 `affine fn`을 받으므로 그런 워커도 받는다.
|
||||
|
||||
그래서 재시작 가능한 워커의 모양이 정해진다: **자원을 밖에서 받지 말고
|
||||
안에서 획득하고 안에서 반납한다.** 풀을 빌려 받을 뿐 lease를 들고 오지
|
||||
않으므로 클로저가 affine이 아니다.
|
||||
|
||||
**③ 격리 경계는 recover가 아니다.** `Death`에 담을 수 있는 것이 메시지뿐이다.
|
||||
죽은 일꾼이 계산하던 값도, 들고 있던 자원도 넘어오지 않는다. **담을 자리가
|
||||
있으면 그것이 recover다** — 이 열거형의 모양이 그 원칙의 코드 수준 의미다.
|
||||
|
||||
### D19. 가져온 모듈의 struct를 만들 수 없었다 — 고침
|
||||
|
||||
```cool
|
||||
sup.supervise(sc, body, Sup.Policy { retries: 3 })
|
||||
^ )이(가) 필요합니다 — { 발견
|
||||
```
|
||||
|
||||
`Alias.Name { ... }`가 문법에 없었다. **타입은 쓸 수 있는데 값은 못 만드는
|
||||
상태**였고, 개밥 넷이 전부 같은 모듈 안에서만 struct를 만들어 한 번도 안
|
||||
걸렸다.
|
||||
|
||||
고치는 과정이 문법 검증 장치의 값을 보여줬다. 처음에 점을 `field_sfx`와
|
||||
`name_or_struct` 양쪽에 두자 **LL(1) 검사가 즉시 진짜 충돌 2건을 냈다** —
|
||||
같은 문장을 두 프로덕션이 읽을 수 있었다. 다음 시도(struct_body를 field_sfx에
|
||||
붙이기)는 LL(1)은 통과했지만 **문법-파서 대조와 생성 커버리지가 잡았다**:
|
||||
문법은 `f().x { }`를 만드는데 파서가 거부했다. 최종형은 struct를 만들 수 있는
|
||||
문맥(`<yes>`)에서만 점을 미리 먹는다. 세 장치가 각각 다른 실수를 잡았다.
|
||||
|
||||
### D20. 가져온 열거형의 이름을 한정 없이 쓰면 조용히 통과했다 — 고침 (보안)
|
||||
|
||||
두 자리에서 각각 다르게 샜다.
|
||||
|
||||
**패턴에서는 바인딩이 됐다.**
|
||||
|
||||
```cool
|
||||
match s {
|
||||
Done => ..., // Sup.Done이어야 한다. 여기서는 그냥 이름 하나다
|
||||
GaveUp(d) => ..., // "이 팔은 도달할 수 없습니다" — 틀린 줄을 가리킨다
|
||||
}
|
||||
```
|
||||
|
||||
규칙 자체는 옳다 ("한정되었으면 언제나 생성자, 아니면 바인딩"). 문제는
|
||||
**진단이 엉뚱한 줄을 가리킨다**는 것이다. 첫 팔이 전부를 잡는 것이 원인인데
|
||||
오류는 둘째 팔에 난다. 이제 이렇게 말한다:
|
||||
|
||||
```
|
||||
Done은(는) 가져온 열거형의 variant입니다 — Sup.Done로 적으십시오
|
||||
(한정 없는 이름은 바인딩입니다)
|
||||
```
|
||||
|
||||
**식에서는 모르는 이름이 됐다. 이쪽이 보안 구멍이다.**
|
||||
|
||||
```cool
|
||||
fn dup(own f: K.Fd) -> K.Box {
|
||||
let a = Wrap(f)
|
||||
Wrap(f) // 통과했다
|
||||
}
|
||||
```
|
||||
|
||||
`Wrap`이 생성자인 줄 모르므로 move 검사의 `is_ctor`가 안 걸리고, 인자가
|
||||
Move가 아니라 빌림이 된다. **affine 값이 복제된다.** D13(생성자가 인자를
|
||||
소비하지 않았다)을 고쳤는데, 가져온 생성자는 애초에 생성자로 인식되지도
|
||||
않고 있었다.
|
||||
|
||||
**Mini Shell에 다섯 군데 있었다.** `Ends(r, w)`, `ToPipe(w)`, `FromPipe(r)`,
|
||||
`InheritIn`, `InheritOut` — 즉 **이 개밥이 시험하려던 파이프 처리 전체가
|
||||
검사되지 않고 있었다.** 고친 뒤 확인하니 이제 진짜로 잡는다:
|
||||
|
||||
```
|
||||
인자: Os.ReadFd이(가) 필요한데 Os.WriteFd입니다
|
||||
w은(는) 이미 move되었습니다 (81:81에서 소비)
|
||||
```
|
||||
|
||||
**이것이 개밥 먹기가 통과했다는 사실을 믿으면 안 되는 이유다.** 합격 기준 ②
|
||||
("모듈이 실제로 해소될 것")는 모듈 수준의 공허함만 막는다. **이름 하나가
|
||||
조용히 불투명해지는 것은 못 막는다.** D1 → D15 → D20이 같은 병의 세 단계였다:
|
||||
모듈 전체 → 점 왼쪽 → 점을 안 찍은 이름.
|
||||
|
||||
### D21. spawn한 태스크의 값이 갈 데가 없다 — 열림
|
||||
|
||||
```cool
|
||||
work.spawn(fn() { announce(log, run_worker(...)) })
|
||||
```
|
||||
|
||||
일꾼이 어떻게 끝났는지 부모가 알 수 없다. 클로저의 반환값이 갈 데가 없기
|
||||
때문이다 — `Future`도 `Task[a]`도 없고, `scope`의 join은 값을 만들지 않는다.
|
||||
|
||||
그래서 보고를 값이 아니라 **Log capability**로 한다. 얼랭도 그렇게 하므로
|
||||
틀린 모양은 아니고, 오히려 "결과는 effect로 나간다"가 감독과 잘 맞는다.
|
||||
다만 **취소와 backpressure에 가면 이것으로 안 될 가능성이 높다** — 되돌려
|
||||
받을 값이 있어야 흐름을 조절할 수 있다.
|
||||
|
||||
Reverse Proxy(#4) 전에 결정해야 한다.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 테스트 하네스의 결함 (2026-08-30)
|
||||
|
||||
개밥 먹기가 아니라 저장소 자체에서 나온 것이므로 따로 적는다.
|
||||
|
||||
`test/test_coollang.ml`의 요약과 `exit 1`이 **파일 184줄**에 있었다. OCaml의
|
||||
최상위 `let ()`은 파일 순서대로 돌므로 **그 뒤에 쓴 1500줄의 검사는 FAIL을
|
||||
찍고도 종료 코드가 0이었다.**
|
||||
|
||||
즉 **실패한 테스트를 커밋할 수 있었다.** 빠른 검증 루프가 목적인 저장소에서
|
||||
검증이 조용히 통과하는 것이 가장 나쁜 결함이다. 요약을 파일 끝으로 옮겼다.
|
||||
|
||||
발견 경위가 이 저장소의 방법론을 그대로 보여준다 — 새 테스트를 넣고
|
||||
"일부러 틀리게 만들어 FAIL이 나는지" 확인하다가 나왔다. 통과를 믿지 않고
|
||||
실패를 확인하는 습관이 아니었으면 안 보였다.
|
||||
|
||||
---
|
||||
|
||||
**순서** (2026-08-30 갱신 — 1·3 완료)
|
||||
|
||||
1. ~~**D4** — 수신자를 소비하는 메서드~~ → 고침. `own self`
|
||||
2. **`resource` 종류와 `with`** — 아래 "with를 미루는 이유" 참고
|
||||
3. ~~Mini Shell~~ → 완료. D13–D18이 여기서 나왔다
|
||||
4. ~~Process Supervisor~~ → 완료. D19–D21. 문서의 주장 셋이 코드가 됐다
|
||||
5. 다음은 Reverse Proxy(#4). 그 전에 D21(태스크의 값)을 결정해야 한다
|
||||
|
||||
**LRU가 더한 제약**: 자원은 렉시컬 전용으로 간다. 자원을 담는 컨테이너는
|
||||
런타임만 만들 수 있다 — 축출처럼 비렉시컬한 정리는 `with`가 못 닿기 때문이다.
|
||||
사용자가 자기 자원 풀을 언어로 못 만드는 것이 그 대가다.
|
||||
|
||||
---
|
||||
|
||||
## `with`를 미루는 이유 (2026-08-30 결정)
|
||||
|
||||
네 개밥을 다 쓰고 나서 내린 결정이다. 셋을 정하고 하나를 미룬다.
|
||||
|
||||
### 1. `resource`라는 새 종류는 넣지 않는다 — 확정
|
||||
|
||||
capability가 이미 그것이다. affine이고, 위조되지 않고, 런타임만 만든다.
|
||||
`resource`가 더하는 사실은 "정리 메서드가 있다" 하나뿐인데, 그것은 `own self`가
|
||||
말한다. 종류를 하나 더 만들면 **95%가 같은 두 종류**가 생기고 그것이 "한 개념
|
||||
한 방식"이 금지하는 모양이다.
|
||||
|
||||
### 2. 누수는 lint로 보인다 — 넣음
|
||||
|
||||
D3의 답을 절반 냈다. `?`와 `return`이 있는 자리에서, 소유하고 있고 아직 아무
|
||||
데도 넘기지 않은 affine 값이 있으면 말한다.
|
||||
|
||||
```
|
||||
?이(가) 여기서 빠져나가면 f이(가) 정리되지 않습니다 (?는 뒤의 정리를 건너뜁니다)
|
||||
```
|
||||
|
||||
**측정**: 저장소 전체에서 네 곳. `atomic-update/naive` 2, `db-pool/naive` 2.
|
||||
**오탐 0건** — careful 판 둘과 samples 전부가 조용하다. naive/careful 쌍이
|
||||
정확히 나뉜다.
|
||||
|
||||
타입 층의 정책은 그대로다 — 오용 금지, 누수 허용. lint는 막지 않고 말한다.
|
||||
|
||||
### 3. `with`는 미룬다 — 설계가 갈라져 있고, 데이터가 아직 부족하다
|
||||
|
||||
두 갈래가 있고 둘 다 그럴듯하다.
|
||||
|
||||
**갈래 A — 자동 반납.** 블록을 나갈 때 런타임이 정리 메서드를 부른다
|
||||
(Python `with`, C# `using`). `?`가 그대로 쓸 수 있게 되고 `careful.cool`
|
||||
58줄이 `naive.cool` 18줄로 접힌다. 대가: **선언되지 않은 effect가 수행된다.**
|
||||
이 언어에서 effect는 시그니처에 다 적혀야 하는데, 안 보이는 호출이 생긴다.
|
||||
그리고 정리 메서드가 둘인 자원(트랜잭션의 commit/abort)을 다루지 못한다.
|
||||
|
||||
**갈래 B — 소비 강제.** 블록이 끝나기 전에 모든 길에서 그 값을 소비해야
|
||||
하고, 아니면 오류다. 아무것도 자동으로 부르지 않는다. 숨은 effect가 없고
|
||||
commit/abort가 자연스럽다. 대가: **마찰이 그대로 남는다.** 컴파일러가
|
||||
"여기서 안 닫았다"고 말해 줄 뿐, 중첩된 match를 손으로 쓰는 일은 똑같다.
|
||||
|
||||
**미루는 이유는 Mini Shell이다.** 셸이 보여준 모양에는 `with`가 닿지 않는다.
|
||||
파이프 하나는 끝이 둘이고 각각 다른 자식에게 간다. 부모에게는 아무것도 안
|
||||
남고, 어느 블록에도 묶이지 않는다. 즉 **가장 어려운 자원 코드가 렉시컬하지
|
||||
않다.** 렉시컬 도구를 먼저 넣으면 그 도구가 닿지 않는 자리를 나중에 다시
|
||||
설계해야 하고, 그때는 이미 두 방식이 공존한다.
|
||||
|
||||
`own self`가 D9이 요구한 것을 이미 줬으므로 급하지 않다. 정리는 자원만으로
|
||||
표현되고, 도우미의 시그니처가 진짜 의존을 말한다.
|
||||
|
||||
**결정을 다시 여는 조건**: Process Supervisor(#3)나 Reverse Proxy(#4)에서
|
||||
렉시컬한 자원 코드가 다수를 차지하면 갈래 A·B 중 하나를 고른다. 셸처럼
|
||||
비렉시컬한 것이 또 나오면 `with`가 아니라 컨테이너 문제(D17)를 먼저 푼다.
|
||||
|
||||
**미루는 것**: crash 시 정리 여부. `with`를 넣어도 이 질문은 열려 있다 —
|
||||
정상 종료·`return`·`?`에서는 돌고 crash에서는 안 도는 것이 지금의 잠정
|
||||
답이지만, 감독(supervision)이 들어오면 다시 봐야 한다.
|
||||
@@ -0,0 +1,83 @@
|
||||
# dogfoods — 실제 유스케이스로 언어를 압박한다
|
||||
|
||||
`samples/`가 검사기를 시험한다면 여기는 **언어가 실제 문제를 표현할 수 있는지**를
|
||||
시험한다. 성격이 다르므로 폴더를 나눈다.
|
||||
|
||||
## 방법
|
||||
|
||||
프로그램을 **쓰되 돌리지 않는다.** `std-draft/`에 런타임이 구현한다고 가정한
|
||||
선언을 두고, 그것에 대고 실제 프로그램을 쓴다.
|
||||
|
||||
"가상"이 아니다. `std/*.cool`이 원래 본문 없는 선언이므로 **실행만 빼고 전부
|
||||
진짜로 검사된다**:
|
||||
|
||||
```
|
||||
lexer ✓ parser ✓ 이름 해소 ✓ 타입 ✓ effect ✓
|
||||
capability ✓ move/affinity ✓ 소진성 ✓ interface hash ✓
|
||||
run ✗
|
||||
```
|
||||
|
||||
종이 스케치가 아니라 컴파일러가 검증한 설계다.
|
||||
|
||||
## 합격 기준
|
||||
|
||||
```
|
||||
① coolc check exit 0
|
||||
② 모듈이 실제로 해소될 것
|
||||
```
|
||||
|
||||
②가 없으면 ①이 공허하다. import가 조용히 해소되지 않으면 그 모듈의 이름이
|
||||
전부 불투명해지고, 검사기는 "모르는 것을 틀렸다고 말하지 않는다"는 원칙에 따라
|
||||
무엇이든 통과시킨다. **실제로 첫 시도에서 이 일이 일어났다** (D1 참고).
|
||||
|
||||
## 규율
|
||||
|
||||
`std-draft`의 선언은 독립적으로 정당화되어야 한다.
|
||||
|
||||
- 지금 런타임이 가진 권한으로 구현 가능할 것
|
||||
- effect를 전부 선언할 것
|
||||
- **실패 양상을 선언하는 자리에서 정할 것** — 무엇이 `Result`이고 무엇이
|
||||
`crash`인지
|
||||
- 없는 언어 기능에 기대지 말 것. 필요하다는 것이 드러나면 **그것이 발견이지
|
||||
지름길이 아니다**
|
||||
|
||||
프로그램이 예뻐 보이도록 API를 발명하면 아무것도 배우지 못한다.
|
||||
|
||||
## 한계
|
||||
|
||||
이 기준이 증명하는 것은 **표현 가능성**이지 의미론의 정확성이 아니다.
|
||||
`check`가 통과한다고 그 프로그램이 옳게 도는 것은 아니다 — 돌려본 적이 없다.
|
||||
|
||||
**Mini Shell에서 그 한계가 반쯤 깨졌다.** 파싱을 자원에서 떼어 놓았더니
|
||||
`mini-shell/parse.cool`은 실제로 돈다 — 내장 test 5개가 `coolc test`로
|
||||
통과한다. 자원을 안 쓰는 부분은 돌릴 수 있다는 뜻이고, 그것은 개밥을
|
||||
어떻게 나눠 써야 하는지를 말해 준다.
|
||||
|
||||
## 세트
|
||||
|
||||
| | | 압박하는 곳 | 상태 |
|
||||
|---|---|---|---|
|
||||
| 1 | **Mini Shell** | **여러 자원, 하나가 둘로 갈라짐** | **완료** |
|
||||
| 2 | **Atomic File Updater** | **자원 수명, 실패 경로** | **완료** |
|
||||
| 3 | **Process Supervisor** | **실패 위상** | **완료** |
|
||||
| 4 | Reverse Proxy | 취소, backpressure | 대기 |
|
||||
| 5 | TCP Server | 규모의 API 모양 | 대기 |
|
||||
| 6 | DB Pool | lease, 자원 정리 | **완료** |
|
||||
| + | **LRU Cache** | **비렉시컬 정리 (축출)** | **완료** |
|
||||
| 7 | Resource Exhaustion | 실패 의미론의 최악 조건 | **미룸** — 런타임 속성이라 안 돌리면 알 수 없다. 언어 층 질문("무엇이 Result이고 무엇이 crash인가")은 std-draft 규율로 흡수했다 |
|
||||
|
||||
순서는 자원 모델(2, 6, 1) → 동시성·취소(3, 4, 5)다. 2번을 먼저 둔 이유는
|
||||
**동시성을 안 섞고 자원 모델만 시험할 수 있는 유일한 것**이기 때문이다.
|
||||
|
||||
자원 모델 쪽 넷이 끝났다. 넷이 각각 다른 모양이었다:
|
||||
|
||||
| | 자원 개수 | 정리 지점 | 한 자원이 |
|
||||
|---|---|---|---|
|
||||
| 파일 | 하나 | 렉시컬 | 한 곳으로 |
|
||||
| 풀 | 두 층 | 렉시컬 | 한 곳으로 |
|
||||
| LRU | 한 종류, 여럿 | 비렉시컬 | 한 곳으로 |
|
||||
| 셸 | 여러 종류, 여럿 | 비렉시컬 | **둘로 갈라져 서로 다른 곳으로** |
|
||||
|
||||
이 넷이 `with`를 미루기로 한 근거다 (FINDINGS의 "`with`를 미루는 이유").
|
||||
|
||||
발견은 `FINDINGS.md`에 쌓고, 확정된 결정은 `docs/thesis.md`로 올린다.
|
||||
@@ -0,0 +1,62 @@
|
||||
// 제대로 정리하는 버전.
|
||||
//
|
||||
// naive.cool과 같은 일을 하지만 모든 실패 경로에서 핸들을 닫고 임시 파일을
|
||||
// 지운다. 두 파일의 차이가 이 개밥 먹기의 산출물이다.
|
||||
//
|
||||
// ※ 2판. `own self` 뒤에 다시 썼다. 도우미들이 더 이상 fs를 들고 다니지
|
||||
// 않는다 — 닫는 데 필요한 것이 핸들 자신뿐이기 때문이다.
|
||||
|
||||
import "../std-draft/fs" as Fs
|
||||
import "cool.dev/std/string" as String
|
||||
|
||||
pub fn update(fs: Fs.Fs, path: String, contents: String)
|
||||
effects {Fs.create, WriteFile.write, WriteFile.sync, WriteFile.close,
|
||||
Fs.rename, Fs.remove}
|
||||
-> Result[Unit, Fs.IoError] {
|
||||
let tmp = String.concat(path, ".tmp")
|
||||
match fs.create(tmp) {
|
||||
Err(e) => Err(e),
|
||||
Ok(f) => finish(fs, f, tmp, path, contents),
|
||||
}
|
||||
}
|
||||
|
||||
// 핸들을 얻은 뒤. 여기서부터 모든 실패 경로가 f를 닫고 tmp를 지워야 한다.
|
||||
// ? 를 쓸 수 없다 — 조기 반환이 정리를 건너뛰기 때문이다.
|
||||
pub fn finish(
|
||||
fs: Fs.Fs,
|
||||
own f: Fs.WriteFile,
|
||||
tmp: String,
|
||||
path: String,
|
||||
contents: String,
|
||||
) effects {WriteFile.write, WriteFile.sync, WriteFile.close, Fs.rename, Fs.remove}
|
||||
-> Result[Unit, Fs.IoError] {
|
||||
match f.write(contents) {
|
||||
Err(e) => abandon(fs, f, tmp, e),
|
||||
Ok(_) => match f.sync() {
|
||||
Err(e) => abandon(fs, f, tmp, e),
|
||||
Ok(_) => match f.close() {
|
||||
Err(e) => discard(fs, tmp, e),
|
||||
Ok(_) => match fs.rename(tmp, path) {
|
||||
Err(e) => discard(fs, tmp, e),
|
||||
Ok(_) => Ok(unit),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 핸들을 아직 들고 있는 실패. 닫고 지운다.
|
||||
// 정리 중의 실패는 일부러 버린다 — 원래 오류가 더 중요하다.
|
||||
// let _ = 가 버린다는 사실을 코드에 보이게 한다.
|
||||
pub fn abandon(fs: Fs.Fs, own f: Fs.WriteFile, tmp: String, e: Fs.IoError)
|
||||
effects {WriteFile.close, Fs.remove} -> Result[Unit, Fs.IoError] {
|
||||
let _ = f.close()
|
||||
discard(fs, tmp, e)
|
||||
}
|
||||
|
||||
// 핸들은 이미 없다. 임시 파일만 지운다.
|
||||
pub fn discard(fs: Fs.Fs, tmp: String, e: Fs.IoError)
|
||||
effects {Fs.remove} -> Result[Unit, Fs.IoError] {
|
||||
let _ = fs.remove(tmp)
|
||||
Err(e)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 순진한 버전 — ?로 짧게 쓴 것.
|
||||
//
|
||||
// 처음 썼을 때 이 파일의 목적은 "통과한다는 사실을 보여주는 것"이었다.
|
||||
// 이제 lint가 두 자리를 가리킨다 — write와 sync의 ?다. 그 둘에서 빠져나가면
|
||||
// f가 닫히지 않는다. 타입 층에서는 여전히 오류가 아니다 (누수 허용).
|
||||
|
||||
import "../std-draft/fs" as Fs
|
||||
import "cool.dev/std/string" as String
|
||||
|
||||
pub fn update(fs: Fs.Fs, path: String, contents: String)
|
||||
effects {Fs.create, WriteFile.write, WriteFile.sync, WriteFile.close, Fs.rename}
|
||||
-> Result[Unit, Fs.IoError] {
|
||||
let tmp = String.concat(path, ".tmp")
|
||||
let f = fs.create(tmp)?
|
||||
f.write(contents)?
|
||||
f.sync()?
|
||||
f.close()?
|
||||
fs.rename(tmp, path)?
|
||||
Ok(unit)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// 제대로 반납하는 버전.
|
||||
//
|
||||
// Atomic Updater와 비교하는 것이 목적이다. 자원이 두 층이고, 반납이
|
||||
// 파괴가 아니며, 획득이 실패할 수 있다 — 파일에는 셋 다 없었다.
|
||||
//
|
||||
// ※ 2판. `own self` 뒤에 다시 썼다. 1판과의 차이는 아래 주석에 적는다.
|
||||
|
||||
import "../std-draft/db" as Db
|
||||
|
||||
// 한 요청. 성공하든 실패하든 lease를 반납한다.
|
||||
pub fn load_user(pool: Db.Pool, sql: String)
|
||||
effects {Pool.acquire, Lease.query, Lease.release}
|
||||
-> Result[List[String], Db.DbError] {
|
||||
match pool.acquire() {
|
||||
Err(e) => Err(e),
|
||||
// 1판에서는 l을 소비하는 인자와 l을 쓰는 인자를 한 호출에 섞을 수
|
||||
// 없어(D8) 순서를 손으로 풀어야 했다. 이제 질의는 수신자를 빌리고
|
||||
// 반납은 수신자를 소비하므로 둘이 같은 인자 목록에 만날 일이 없다.
|
||||
Ok(l) => {
|
||||
let r = l.query(sql)
|
||||
release_then(l, r)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 반납하고 원래 결과를 그대로 돌려준다.
|
||||
// 반납 실패는 일부러 버린다 — 원래 결과가 더 중요하다.
|
||||
//
|
||||
// 1판은 (db, pool, l, r) 넷을 받았다. 이제 둘이다 — 반납에 필요한 것이
|
||||
// lease 자신뿐이기 때문이다. 도우미의 시그니처가 정리의 진짜 의존을 말한다.
|
||||
pub fn release_then(
|
||||
own l: Db.Lease,
|
||||
r: Result[List[String], Db.DbError],
|
||||
) effects {Lease.release} -> Result[List[String], Db.DbError] {
|
||||
let _ = l.release()
|
||||
r
|
||||
}
|
||||
|
||||
// 프로그램 하나. 풀을 열고 쓰고 닫는다.
|
||||
pub fn run(db: Db.Db, url: String, sql: String)
|
||||
effects {Db.pool, Pool.acquire, Pool.close, Lease.query, Lease.release}
|
||||
-> Result[List[String], Db.DbError] {
|
||||
match db.pool(url, 8) {
|
||||
Err(e) => Err(e),
|
||||
Ok(pool) => {
|
||||
let rows = load_user(pool, sql)
|
||||
close_then(pool, rows)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_then(
|
||||
own pool: Db.Pool,
|
||||
r: Result[List[String], Db.DbError],
|
||||
) effects {Pool.close} -> Result[List[String], Db.DbError] {
|
||||
let _ = pool.close()
|
||||
r
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// 순진한 버전 — ?로 짧게 쓴 것.
|
||||
//
|
||||
// Atomic Updater와 같은 병이 나오는지 본다. 다만 자원이 두 층이다 —
|
||||
// 풀과 lease. 그리고 반납은 파괴가 아니다.
|
||||
//
|
||||
// lint가 두 층을 각각 가리킨다: query의 ?에서 lease가, load_user의 ?에서
|
||||
// 풀이 샌다. 층이 늘면 새는 자리도 그만큼 는다.
|
||||
|
||||
import "../std-draft/db" as Db
|
||||
|
||||
// 한 요청. lease를 빌리고, 질의하고, 반납한다.
|
||||
pub fn load_user(pool: Db.Pool, sql: String)
|
||||
effects {Pool.acquire, Lease.query, Lease.release}
|
||||
-> Result[List[String], Db.DbError] {
|
||||
let l = pool.acquire()?
|
||||
let rows = l.query(sql)? // 실패하면 lease가 반납되지 않는다
|
||||
l.release()?
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
// 프로그램 하나. 풀을 열고 쓰고 닫는다.
|
||||
pub fn run(db: Db.Db, url: String, sql: String)
|
||||
effects {Db.pool, Pool.acquire, Pool.close, Lease.query, Lease.release}
|
||||
-> Result[List[String], Db.DbError] {
|
||||
let pool = db.pool(url, 8)?
|
||||
let rows = load_user(pool, sql)? // 실패하면 풀도 안 닫힌다
|
||||
pool.close()?
|
||||
Ok(rows)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// LRU 캐시 — 자원 판.
|
||||
//
|
||||
// 앞의 두 개밥(파일, 풀)은 정리 지점이 렉시컬이었다. 여기는 아니다.
|
||||
// 축출은 삽입의 부작용으로, 예측할 수 없는 때에 일어난다.
|
||||
//
|
||||
// 이것이 with 결정 전에 알아야 할 자리다 — with는 렉시컬 수명만 다루는데
|
||||
// 축출된 자원은 어느 블록에도 묶이지 않는다.
|
||||
|
||||
import "cool.dev/std/list" as List
|
||||
import "../std-draft/db" as Db
|
||||
|
||||
pub struct Held {
|
||||
key: String,
|
||||
lease: Db.Lease,
|
||||
}
|
||||
|
||||
// Lease를 필드로 가지므로 전이적으로 affine이다 — 선언하지 않아도 유도된다.
|
||||
pub struct LeaseCache {
|
||||
cap: Int,
|
||||
held: List[Held],
|
||||
}
|
||||
|
||||
// put은 캐시와 "떨어져 나온 자원"을 함께 돌려줘야 한다. 튜플이 없다.
|
||||
//
|
||||
// struct로 쓰면 막힌다 — 필드 접근은 빌림이라 affine 값을 꺼낼 수 없고,
|
||||
// struct를 분해하는 패턴이 언어에 없다. 열거형은 패턴이 분해하므로 꺼낼 수
|
||||
// 있다. 그래서 "둘을 함께 돌려주기"가 열거형으로만 가능하다.
|
||||
pub enum Put {
|
||||
Kept(LeaseCache),
|
||||
Evicted(LeaseCache, Db.Lease),
|
||||
}
|
||||
|
||||
pub fn empty(cap: Int) -> LeaseCache {
|
||||
LeaseCache { cap: cap, held: [] }
|
||||
}
|
||||
|
||||
// 넣는다. 넘치면 가장 오래된 것이 떨어져 나온다.
|
||||
//
|
||||
// 떨어져 나온 lease는 호출자가 반납해야 한다. 언어는 그것을 강제하지 못한다 —
|
||||
// Put.evicted를 무시하고 버려도 아무 말이 없다. 누수 허용이므로.
|
||||
pub fn put(own c: LeaseCache, key: String, own l: Db.Lease) -> Put
|
||||
|
||||
// 호출자가 해야 하는 일. 이 함수를 안 부르면 조용히 샌다.
|
||||
pub fn drain(own p: Put) effects {Lease.release} -> LeaseCache {
|
||||
match p {
|
||||
Kept(c) => c,
|
||||
Evicted(c, l) => release_then(l, c),
|
||||
}
|
||||
}
|
||||
|
||||
// ※ 2판. `own self` 뒤로 db도 pool도 필요 없다 — 반납은 lease 자신이 안다.
|
||||
pub fn release_then(own l: Db.Lease, own c: LeaseCache)
|
||||
effects {Lease.release} -> LeaseCache {
|
||||
let _ = l.release()
|
||||
c
|
||||
}
|
||||
|
||||
// 쓰는 쪽. 매 삽입마다 축출을 받아 처리해야 한다.
|
||||
pub fn add(pool: Db.Pool, own c: LeaseCache, key: String)
|
||||
effects {Pool.acquire, Lease.release} -> Result[LeaseCache, Db.DbError] {
|
||||
match pool.acquire() {
|
||||
Err(e) => Err(e),
|
||||
Ok(l) => Ok(drain(put(c, key, l))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(c: LeaseCache) -> Int {
|
||||
List.len(c.held)
|
||||
}
|
||||
@@ -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],
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Mini Shell — 줄을 파이프라인으로 읽는 부분.
|
||||
//
|
||||
// 이 파일에는 자원이 하나도 없다. 일부러 나눴다 — 파싱은 순수하므로
|
||||
// 내장 test로 그 자리에서 증명할 수 있고, 자원이 섞이면 그러지 못한다.
|
||||
// 테스트는 capability를 받지 않으므로 effect-free임이 검사로 증명된다.
|
||||
|
||||
import "cool.dev/std/list" as List
|
||||
import "cool.dev/std/string" as String
|
||||
import "cool.dev/std/option" as Option
|
||||
import "cool.dev/std/result" as Result
|
||||
import "cool.dev/std/test" as Test
|
||||
|
||||
// 명령 하나. program과 args를 나눠 둔다 —
|
||||
// argv[0]에 이름을 겹쳐 두는 관례는 둘 중 뭐가 진짜인지 매번 묻게 만든다.
|
||||
pub copyable struct Command {
|
||||
program: String,
|
||||
args: List[String],
|
||||
}
|
||||
|
||||
// 파이프로 이어진 명령들. 비어 있을 수 있다 (빈 줄).
|
||||
pub copyable struct Pipeline {
|
||||
stages: List[Command],
|
||||
}
|
||||
|
||||
// 무엇이 잘못됐는지. 셸은 오류를 사람에게 보여주고 계속 돈다 —
|
||||
// 죽지 않는다. 그래서 crash가 아니라 Result다.
|
||||
pub enum ParseError {
|
||||
// "ls | | wc" — 가운데가 비었다
|
||||
EmptyStage,
|
||||
// "|" 로 시작하거나 끝났다
|
||||
DanglingPipe,
|
||||
}
|
||||
|
||||
// 공백으로 쪼개고 빈 조각을 버린다.
|
||||
// 연속된 공백이 빈 조각을 만들기 때문에 filter가 필요하다.
|
||||
pub fn words(s: String) -> List[String] {
|
||||
List.filter(String.split(String.trim(s), " "), fn(w) { !String.is_empty(w) })
|
||||
}
|
||||
|
||||
// 조각 하나를 명령으로. 조각이 비면 EmptyStage다.
|
||||
pub fn stage(s: String) -> Result[Command, ParseError] {
|
||||
let ws = words(s)
|
||||
match List.first(ws) {
|
||||
None => Err(EmptyStage),
|
||||
Some(p) => Ok(Command { program: p, args: List.rest(ws) }),
|
||||
}
|
||||
}
|
||||
|
||||
// 줄 하나를 파이프라인으로.
|
||||
//
|
||||
// 빈 줄은 오류가 아니다 — 사람이 엔터만 친 것이고, 셸은 아무 일도 하지
|
||||
// 않는다. 그것과 "ls | | wc"는 다른 일이므로 구분한다.
|
||||
pub fn line(raw: String) -> Result[Pipeline, ParseError] {
|
||||
let t = String.trim(raw)
|
||||
if String.is_empty(t) {
|
||||
Ok(Pipeline { stages: [] })
|
||||
} else {
|
||||
stages(String.split(t, "|"))
|
||||
}
|
||||
}
|
||||
|
||||
// 조각들을 차례로 읽는다. 하나라도 실패하면 전체가 실패한다.
|
||||
//
|
||||
// fold로 접는다. 누적자가 Result라 실패가 그대로 실려간다 — 조기 탈출이
|
||||
// 없어도 뒤 조각이 앞의 실패를 덮지 않는다.
|
||||
pub fn stages(parts: List[String]) -> Result[Pipeline, ParseError] {
|
||||
List.fold(parts, Ok(Pipeline { stages: [] }), fn(own acc, part) {
|
||||
match acc {
|
||||
Err(e) => Err(e),
|
||||
Ok(p) => match stage(part) {
|
||||
Err(e) => Err(e),
|
||||
Ok(c) => Ok(Pipeline { stages: List.push(p.stages, c) }),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 내장 명령인가. cd와 exit은 자식 프로세스로 띄우면 뜻이 없다 —
|
||||
// 자식의 디렉터리를 바꿔 봐야 자식이 죽으면 사라진다.
|
||||
pub fn is_builtin(c: Command) -> Bool {
|
||||
c.program == "cd" || c.program == "exit"
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 테스트
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
test "공백은 몇 칸이든 한 번 자른다" {
|
||||
Test.assert(List.len(words(" ls -l ")) == 2)
|
||||
Test.assert(Option.unwrap_or(List.first(words(" ls -l ")), "") == "ls")
|
||||
}
|
||||
|
||||
test "빈 줄은 아무 단계도 없는 파이프라인이다" {
|
||||
match line(" ") {
|
||||
Err(_) => Test.assert(false),
|
||||
Ok(p) => Test.assert(List.is_empty(p.stages)),
|
||||
}
|
||||
}
|
||||
|
||||
test "파이프가 단계를 나눈다" {
|
||||
match line("ls -l | grep foo | wc -l") {
|
||||
Err(_) => Test.assert(false),
|
||||
Ok(p) => Test.assert(List.len(p.stages) == 3),
|
||||
}
|
||||
}
|
||||
|
||||
test "빈 단계는 오류다" {
|
||||
Test.assert(!Result.is_ok(line("ls | | wc")))
|
||||
Test.assert(!Result.is_ok(line("| ls")))
|
||||
Test.assert(!Result.is_ok(line("ls |")))
|
||||
}
|
||||
|
||||
test "program과 args가 나뉜다" {
|
||||
match stage("grep -i foo") {
|
||||
Err(_) => Test.assert(false),
|
||||
Ok(c) => {
|
||||
Test.assert(c.program == "grep")
|
||||
Test.assert(List.len(c.args) == 2)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// Mini Shell — 자원을 다루는 부분.
|
||||
//
|
||||
// 앞의 세 개밥(파일, 풀, LRU)이 못 건드린 데를 친다.
|
||||
// 파일: 자원 하나, 렉시컬
|
||||
// 풀: 자원 두 층, 렉시컬
|
||||
// LRU: 정리 지점이 비렉시컬. 다만 자원은 한 종류
|
||||
// 여기: **자원이 여럿이고, 하나가 둘로 갈라져 서로 다른 곳으로 간다**
|
||||
//
|
||||
// 파이프 하나는 끝이 둘이고 각각 다른 자식에게 간다. 부모는 넘긴 뒤
|
||||
// 아무것도 안 남는다. 어느 블록에도 묶이지 않는다 — `with`가 닿지 않는
|
||||
// 모양이 무엇인지 여기서 정확히 보인다.
|
||||
|
||||
import "cool.dev/std/list" as List
|
||||
import "../std-draft/os" as Os
|
||||
import "./parse" as Parse
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 한 줄 실행
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
// 파이프라인 하나를 돌리고 마지막 단계의 종료 코드를 준다.
|
||||
//
|
||||
// 빈 파이프라인(사람이 엔터만 침)은 0이다. 아무 일도 안 한 것은 성공이다.
|
||||
pub fn run(os: Os.Os, p: Parse.Pipeline)
|
||||
effects {Os.pipe, Os.spawn, Child.wait, ReadFd.close, WriteFd.close}
|
||||
-> Result[Int, Os.OsError] {
|
||||
if List.is_empty(p.stages) {
|
||||
Ok(0)
|
||||
} else {
|
||||
from(os, p.stages, Os.InheritIn)
|
||||
}
|
||||
}
|
||||
|
||||
// 단계 목록을 앞에서부터 실행한다. stdin은 앞 단계가 정해 준다.
|
||||
//
|
||||
// 재귀가 필요하다. 자식을 리스트에 모아 두고 나중에 한꺼번에 거두는 쪽이
|
||||
// 자연스러운데 그렇게 못 한다 — 아래 "리스트에 넣으면 못 꺼낸다" 참고.
|
||||
// 그래서 "뒤를 먼저 다 띄우고, 돌아오면서 자기 자식을 거둔다"가 된다.
|
||||
pub fn from(os: Os.Os, stages: List[Parse.Command], own stdin: Os.Stdin)
|
||||
effects {Os.pipe, Os.spawn, Child.wait, ReadFd.close, WriteFd.close}
|
||||
-> Result[Int, Os.OsError] {
|
||||
match List.first(stages) {
|
||||
// 여기 오면 stdin이 샌다. 언어는 말하지 않는다 (D3).
|
||||
None => Ok(0),
|
||||
Some(c) => {
|
||||
let tail = List.rest(stages)
|
||||
if List.is_empty(tail) {
|
||||
last(os, c, stdin)
|
||||
} else {
|
||||
middle(os, c, stdin, tail)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막 단계. 출력은 부모 것을 그대로 쓴다.
|
||||
pub fn last(os: Os.Os, c: Parse.Command, own stdin: Os.Stdin)
|
||||
effects {Os.spawn, Child.wait} -> Result[Int, Os.OsError] {
|
||||
match os.spawn(c.program, c.args, stdin, Os.InheritOut) {
|
||||
Err(e) => Err(e),
|
||||
Ok(child) => child.wait(),
|
||||
}
|
||||
}
|
||||
|
||||
// 가운데 단계. 파이프를 만들어 쓰는 끝을 자식에게 주고,
|
||||
// 읽는 끝을 다음 단계에 넘긴다.
|
||||
//
|
||||
// 이 함수가 이 개밥 먹기의 본체다. 자원 셋(stdin, 읽는 끝, 쓰는 끝)이
|
||||
// 서로 다른 곳으로 가고, 실패하면 셋의 상태가 제각각이다.
|
||||
pub fn middle(
|
||||
os: Os.Os,
|
||||
c: Parse.Command,
|
||||
own stdin: Os.Stdin,
|
||||
rest: List[Parse.Command],
|
||||
) effects {Os.pipe, Os.spawn, Child.wait, ReadFd.close, WriteFd.close}
|
||||
-> Result[Int, Os.OsError] {
|
||||
match os.pipe() {
|
||||
// stdin이 샌다
|
||||
Err(e) => Err(e),
|
||||
Ok(p) => match p {
|
||||
Os.Ends(r, w) => match os.spawn(c.program, c.args, stdin, Os.ToPipe(w)) {
|
||||
// 여기서는 r이 샌다. w는 spawn이 가져갔는지 아닌지
|
||||
// 시그니처만 봐서는 알 수 없다 — 아래 발견 참고.
|
||||
Err(e) => Err(e),
|
||||
// 자식이 떴다. 부모는 쓰는 끝을 이미 넘겼고, 읽는 끝을
|
||||
// 다음 단계에 넘긴다. 부모에게는 아무것도 안 남는다.
|
||||
Ok(child) => reap(child, from(os, rest, Os.FromPipe(r))),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 자식을 거두고 뒤쪽의 결과를 돌려준다.
|
||||
//
|
||||
// 뒤쪽 결과가 앞선다 — 파이프라인의 종료 코드는 마지막 단계의 것이다.
|
||||
// 앞 단계가 죽어도 셸은 마지막 것을 보고한다.
|
||||
//
|
||||
// 거두기 실패는 일부러 버린다. 자식이 이미 사라진 것은 우리가 할 일이
|
||||
// 없고, 뒤쪽 결과가 사람이 알고 싶은 것이다.
|
||||
pub fn reap(own child: Os.Child, r: Result[Int, Os.OsError])
|
||||
effects {Child.wait} -> Result[Int, Os.OsError] {
|
||||
let _ = child.wait()
|
||||
r
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 되풀이
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
// 읽고, 읽어서 나눈 것을 돌리고, 다시 읽는다.
|
||||
//
|
||||
// 내장 명령은 자식으로 띄우지 않는다. cd를 자식에서 하면 자식의 디렉터리가
|
||||
// 바뀌고 자식은 곧 죽는다 — 아무 일도 안 일어난 것과 같다.
|
||||
pub fn once(os: Os.Os, line: String)
|
||||
effects {Os.pipe, Os.spawn, Os.chdir, Child.wait,
|
||||
ReadFd.close, WriteFd.close}
|
||||
-> Result[Int, Os.OsError] {
|
||||
match Parse.line(line) {
|
||||
// 파싱 실패는 셸을 죽이지 않는다. 사람이 오타를 낸 것뿐이다.
|
||||
Err(_) => Ok(2),
|
||||
Ok(p) => match List.first(p.stages) {
|
||||
None => Ok(0),
|
||||
Some(head) => {
|
||||
if Parse.is_builtin(head) && List.len(p.stages) == 1 {
|
||||
builtin(os, head)
|
||||
} else {
|
||||
run(os, p)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 내장 명령. cd만 실제로 뭔가 한다.
|
||||
//
|
||||
// exit은 여기서 처리할 수 없다 — 되풀이를 멈추는 일은 부르는 쪽의
|
||||
// 제어 흐름이지 이 함수의 결과값이 아니다. 종료 코드로 신호를 보낸다.
|
||||
pub fn builtin(os: Os.Os, c: Parse.Command)
|
||||
effects {Os.chdir} -> Result[Int, Os.OsError] {
|
||||
if c.program == "exit" {
|
||||
Ok(0 - 1)
|
||||
} else {
|
||||
match List.first(c.args) {
|
||||
None => Ok(0),
|
||||
Some(dir) => match os.chdir(dir) {
|
||||
Err(e) => Err(e),
|
||||
Ok(_) => Ok(0),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// 데이터베이스 연결 풀 — 개밥 먹기용 초안.
|
||||
//
|
||||
// 세 층이 있고 수명을 관리하는 주체가 각각 다르다.
|
||||
// Conn 실제 연결. 풀이 소유한다. 사용자는 존재도 모른다
|
||||
// Pool 연결 묶음. 프로그램 수명. 사용자가 만들고 닫는다
|
||||
// Lease 빌린 한 자리. 요청 하나 수명. 반드시 반납해야 한다
|
||||
//
|
||||
// 사용자가 관리하는 것은 연결의 수명이 아니라 lease의 수명이다.
|
||||
// 그래서 반납은 "파괴"가 아니다 — 연결은 풀로 돌아간다.
|
||||
//
|
||||
// ※ 2판. `own self`가 생긴 뒤 다시 썼다. 전에는 정리가 전부 Db의 메서드라
|
||||
// 반납 한 번에 풀과 권한을 같이 들고 다녀야 했다. 이제 자원이 자기
|
||||
// 정리를 스스로 선언한다.
|
||||
|
||||
// ※ List[String]의 List는 내장 타입이라 import가 필요 없다. 모듈 List를
|
||||
// 가져오는 것은 그 타입에 딸린 함수(len, map...)를 쓸 때뿐이다.
|
||||
|
||||
pub enum DbError {
|
||||
// 연결 자체가 안 된다
|
||||
Unreachable(String),
|
||||
// 풀이 가득 찼고 기다릴 수 없다. 호출자가 대처할 수 있다 — 재시도든 포기든
|
||||
Exhausted,
|
||||
// 질의가 틀렸다. 프로그램의 결함에 가깝지만 런타임 값에서 오므로 Result다
|
||||
BadQuery(String),
|
||||
Other(String),
|
||||
}
|
||||
|
||||
// 빌린 한 자리. 반드시 반납해야 한다.
|
||||
// ※ 언어는 아직 그 "반드시"를 강제하지 못한다 (D3). `with`가 그 자리다.
|
||||
pub capability Lease {
|
||||
// 질의한다. 수신자를 빌린다 — 한 lease로 여러 번 질의할 수 있다.
|
||||
fn query(sql: String) effects {Lease.query} -> Result[List[String], DbError]
|
||||
|
||||
// 반납한다. 연결은 풀로 돌아간다 — 파괴가 아니다.
|
||||
// 수신자를 소비하므로 두 번 반납할 수 없다. 그리고 이 시그니처에는
|
||||
// 풀도 Db 권한도 없다 — 반납에 필요한 것은 lease 자신뿐이다.
|
||||
// 그것이 `with`가 성립하기 위한 조건이다 (D9).
|
||||
fn release(own self) effects {Lease.release} -> Result[Unit, DbError]
|
||||
}
|
||||
|
||||
// 풀. capability이므로 affine이다 — 복제되지 않는다.
|
||||
pub capability Pool {
|
||||
// 한 자리를 빌린다. 없으면 Exhausted — 기다리지 않는다.
|
||||
// 기다리는 형태(timeout)는 취소를 정해야 해서 미룬다.
|
||||
fn acquire() effects {Pool.acquire} -> Result[Lease, DbError]
|
||||
|
||||
// 풀을 닫는다. 안에 있는 연결이 전부 정리된다.
|
||||
// 아직 빌려나간 lease가 있으면? — 아래 발견 참고.
|
||||
fn close(own self) effects {Pool.close} -> Result[Unit, DbError]
|
||||
}
|
||||
|
||||
// 데이터베이스에 손댈 권한. 풀을 만들 수 있는 유일한 출처다.
|
||||
pub capability Db {
|
||||
// 풀을 연다. size는 최대 동시 연결 수.
|
||||
fn pool(url: String, size: Int) effects {Db.pool} -> Result[Pool, DbError]
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// 파일 시스템 — 개밥 먹기용 초안.
|
||||
//
|
||||
// 이 파일은 구현이 아니라 계약이다. 런타임이 구현한다고 가정하고, coollang이
|
||||
// 이 일을 표현할 수 있는지만 본다. 실행은 안 되지만 타입·effect·capability·
|
||||
// 소유권 검사는 전부 진짜로 돈다.
|
||||
//
|
||||
// 규율: 여기 적는 것은 지금 런타임이 가진 권한으로 구현 가능해야 하고,
|
||||
// effect를 전부 선언해야 하며, 없는 언어 기능에 기대면 안 된다. 없는 기능이
|
||||
// 필요하다는 게 드러나면 그것이 발견이지 지름길이 아니다.
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 실패는 무엇인가
|
||||
//
|
||||
// 여기서 Result와 crash의 선을 긋는다. 기준은 "호출자가 대처할 수 있는가"다.
|
||||
// 대처할 수 있다 → Result
|
||||
// 프로그램이 틀렸다 → crash
|
||||
//
|
||||
// 그래서 아래는 전부 Result다. 파일이 없는 것도, 권한이 없는 것도, 디스크가
|
||||
// 가득 찬 것도 프로그램의 결함이 아니다 — 세상의 상태다.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
pub enum IoError {
|
||||
NotFound(String),
|
||||
Denied(String),
|
||||
Exists(String),
|
||||
NoSpace(String),
|
||||
// 나머지. 런타임이 분류하지 못한 것들
|
||||
Other(String),
|
||||
}
|
||||
|
||||
// 열린 쓰기 핸들.
|
||||
//
|
||||
// capability로 선언하는 이유가 둘이다.
|
||||
// 1. 이것은 실제로 권한이다 — 이 파일에 쓸 수 있는 권한
|
||||
// 2. capability는 affinity의 뿌리이므로 복제되지 않는다
|
||||
//
|
||||
// ※ 2판. `own self`가 생긴 뒤 다시 썼다. 1판에서는 핸들에 메서드가 하나도
|
||||
// 없었다 — close가 핸들을 소비해야 하는데 수신자를 소비할 방법이 없어서
|
||||
// 전부 Fs의 메서드로 밀어냈다. 이제 핸들이 자기 메서드를 갖는다.
|
||||
//
|
||||
// 그러면서 권한의 모양이 바뀌었다. 쓰기·동기화·닫기에 더 이상 Fs가 필요
|
||||
// 없다. 핸들만 넘기면 그 파일에 쓸 권한만 넘어가고 파일 시스템 전체 권한은
|
||||
// 넘어가지 않는다 — 권한이 좁아진 채로 전달된다. 1판에서는 쓰게 하려면
|
||||
// Fs를 통째로 줘야 했다.
|
||||
pub capability WriteFile {
|
||||
// 핸들을 빌린다. 여러 번 쓸 수 있다.
|
||||
fn write(s: String) effects {WriteFile.write} -> Result[Unit, IoError]
|
||||
|
||||
// 디스크까지 내려간다. 이게 없으면 rename이 원자적이어도 내용이 없을 수 있다.
|
||||
fn sync() effects {WriteFile.sync} -> Result[Unit, IoError]
|
||||
|
||||
// 수신자를 소비한다. 두 번 닫을 수 없다 — own self가 그것을 강제한다.
|
||||
// 이 시그니처에 Fs가 없다는 것이 요점이다. 정리에 필요한 것이 자원
|
||||
// 자신뿐이어야 `with`가 자원만 보고 무엇을 부를지 알 수 있다 (D9).
|
||||
fn close(own self) effects {WriteFile.close} -> Result[Unit, IoError]
|
||||
}
|
||||
|
||||
// 파일 시스템에 손댈 권한. 이름으로 하는 일이 여기 남는다 —
|
||||
// 경로는 파일 시스템 전체에 대한 이름이므로 전체 권한이 필요하다.
|
||||
pub capability Fs {
|
||||
// 새로 만든다. 이미 있으면 자른다.
|
||||
fn create(path: String) effects {Fs.create} -> Result[WriteFile, IoError]
|
||||
|
||||
// 같은 파일 시스템 안에서 원자적이다.
|
||||
fn rename(from: String, to: String) effects {Fs.rename} -> Result[Unit, IoError]
|
||||
|
||||
fn remove(path: String) effects {Fs.remove} -> Result[Unit, IoError]
|
||||
|
||||
fn read(path: String) effects {Fs.read} -> Result[String, IoError]
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// 프로세스와 파이프 — 개밥 먹기용 초안.
|
||||
//
|
||||
// 이 파일은 구현이 아니라 계약이다. Mini Shell을 쓰기 위해 필요한 최소한만
|
||||
// 적는다. 규율은 fs.cool과 같다 — 없는 언어 기능에 기대면 그것이 발견이다.
|
||||
//
|
||||
// 여기서 처음 나오는 것: **자원의 수명이 렉시컬이 아니다.**
|
||||
// 파이프 하나는 끝이 둘이고, 각각 다른 자식에게 간다. 부모는 넘긴 뒤
|
||||
// 자기 몫을 닫아야 한다. 어느 블록에도 묶이지 않는다.
|
||||
|
||||
pub enum OsError {
|
||||
NotFound(String),
|
||||
Denied(String),
|
||||
// fd가 동났다 / 프로세스가 동났다. 세상의 상태지 프로그램의 결함이 아니다
|
||||
Exhausted(String),
|
||||
Other(String),
|
||||
}
|
||||
|
||||
// 열린 파일 서술자. 읽는 쪽과 쓰는 쪽을 타입으로 나눈다 —
|
||||
// 읽는 끝에 쓰는 것은 프로그램의 결함이고, 결함은 타입이 막는 것이 낫다.
|
||||
pub capability ReadFd {
|
||||
fn close(own self) effects {ReadFd.close} -> Result[Unit, OsError]
|
||||
}
|
||||
|
||||
pub capability WriteFd {
|
||||
fn close(own self) effects {WriteFd.close} -> Result[Unit, OsError]
|
||||
}
|
||||
|
||||
// 파이프. 만들면 끝이 둘 생긴다.
|
||||
//
|
||||
// struct로 담으면 필드에서 꺼낼 수 없다 (부분 move가 없다). 열거형으로
|
||||
// 감싸면 패턴이 분해하므로 꺼낼 수 있다. 그래서 "둘을 함께 돌려주기"가
|
||||
// 열거형으로만 된다 — LRU에서 나온 것과 같은 자리다 (D11).
|
||||
pub enum Pipe {
|
||||
Ends(ReadFd, WriteFd),
|
||||
}
|
||||
|
||||
// 살아 있는 자식 프로세스. 반드시 거둬야 한다 — 안 거두면 좀비가 된다.
|
||||
pub capability Child {
|
||||
// 끝날 때까지 기다리고 종료 코드를 준다. 수신자를 소비한다 —
|
||||
// 한 번 거둔 자식을 다시 거둘 수 없다.
|
||||
fn wait(own self) effects {Child.wait} -> Result[Int, OsError]
|
||||
}
|
||||
|
||||
// 자식에게 물려줄 표준 입출력. 무엇을 물려줄지는 세 가지뿐이다.
|
||||
//
|
||||
// 물려주는 fd는 소비된다 — 자식에게 넘어가고 부모는 더 못 쓴다.
|
||||
// 그것이 own이 시그니처에 있는 이유다.
|
||||
//
|
||||
// ※ 발견: variant 이름이 모듈 전역이라 Stdin.Inherit와 Stdout.Inherit가
|
||||
// 충돌한다. 그래서 InheritIn / InheritOut이 됐다 — 뜻이 아니라 언어의
|
||||
// 제약이 고른 이름이다. 철학 6이 지지 않아야 할 방식으로 진 자리다.
|
||||
pub enum Stdin {
|
||||
// 부모 것을 그대로
|
||||
InheritIn,
|
||||
// 파이프의 읽는 끝
|
||||
FromPipe(ReadFd),
|
||||
}
|
||||
|
||||
pub enum Stdout {
|
||||
InheritOut,
|
||||
ToPipe(WriteFd),
|
||||
}
|
||||
|
||||
// 프로세스를 만들 권한.
|
||||
pub capability Os {
|
||||
fn pipe() effects {Os.pipe} -> Result[Pipe, OsError]
|
||||
|
||||
// 프로그램을 띄운다. argv[0]은 프로그램 이름이 아니라 인자다 —
|
||||
// 이름은 따로 받는다. 관례로 겹쳐 두면 둘 중 뭐가 진짜인지 매번 묻게 된다.
|
||||
fn spawn(
|
||||
program: String,
|
||||
args: List[String],
|
||||
own stdin: Stdin,
|
||||
own stdout: Stdout,
|
||||
) effects {Os.spawn} -> Result[Child, OsError]
|
||||
|
||||
// 현재 디렉터리를 바꾼다. 프로세스 전역 상태이므로 권한이 필요하다.
|
||||
fn chdir(path: String) effects {Os.chdir} -> Result[Unit, OsError]
|
||||
|
||||
fn cwd() effects {Os.cwd} -> String
|
||||
}
|
||||
|
||||
// 줄 단위 입력. 끝나면 None.
|
||||
pub capability Stdin_ {
|
||||
fn read_line() effects {Stdin_.read_line} -> Result[Option[String], OsError]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// 감독 — 개밥 먹기용 초안.
|
||||
//
|
||||
// 문서의 주장을 코드로 옮겨 본다: "spawn은 하나의 개념이고 실패가 어디까지
|
||||
// 가느냐가 권한으로 갈린다." 그 말이 실제 시그니처가 되는지가 이 파일의 목적이다.
|
||||
//
|
||||
// 규율은 앞의 셋과 같다. 없는 언어 기능에 기대면 그것이 발견이다.
|
||||
|
||||
// 죽었다는 사실. 값이 아니라 사실만 넘어온다.
|
||||
//
|
||||
// 격리 경계는 recover가 아니다 — 실패한 계산이 만든 값은 경계를 넘지
|
||||
// 못하고, 경계가 얻는 것은 죽었다는 것과 메시지뿐이다. 그래서 이 열거형에
|
||||
// 자식의 결과를 담는 자리가 없다. 담을 자리가 있으면 그것이 recover다.
|
||||
pub enum Death {
|
||||
Crashed(String),
|
||||
}
|
||||
|
||||
// 왜 멈췄는가. 감독자가 사람에게 보고할 것.
|
||||
pub enum Stopped {
|
||||
// 스스로 끝났다
|
||||
Done,
|
||||
// 죽었고 다시 안 띄운다
|
||||
GaveUp(Death),
|
||||
}
|
||||
|
||||
// 재시작 정책. 얼랭의 OTP가 라이브러리인 것과 같은 이유로 값이다 —
|
||||
// 언어가 정할 일이 아니다.
|
||||
pub copyable struct Policy {
|
||||
// 몇 번까지 다시 띄우는가. 0이면 한 번 죽으면 끝이다.
|
||||
retries: Int,
|
||||
}
|
||||
|
||||
// 감독할 권한.
|
||||
//
|
||||
// TaskScope와 따로 있는 것이 요점이다. sc.spawn(f)는 실패가 부모로
|
||||
// 전파되고, sup.supervise(sc, f)는 여기서 멈춘다. **실패를 삼키려면
|
||||
// Supervisor를 받았어야 하고, 그것이 시그니처에 보인다.**
|
||||
pub capability Supervisor {
|
||||
// 죽으면 다시 띄운다.
|
||||
//
|
||||
// body가 무표기 fn인 것이 이 시그니처의 전부다. 무표기 fn은 여러 번
|
||||
// 부를 수 있고, affine 값을 capture한 클로저는 여기 못 온다 —
|
||||
// 한 번밖에 못 도는 것을 재시작할 수는 없기 때문이다.
|
||||
// 얼랭이 관례로 지키는 것을 affinity가 검사한다.
|
||||
fn supervise[e: effects](
|
||||
sc: TaskScope,
|
||||
body: fn() effects e,
|
||||
policy: Policy,
|
||||
) effects {Supervisor.supervise} | e -> Stopped
|
||||
|
||||
// 한 번만 돌린다. 죽으면 그것으로 끝이고 사실만 돌려준다.
|
||||
// 재시작이 없으므로 자원을 들고 가는 클로저도 된다.
|
||||
fn once[e: effects](
|
||||
sc: TaskScope,
|
||||
body: affine fn() effects e,
|
||||
) effects {Supervisor.once} | e -> Stopped
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Process Supervisor — 죽는 일꾼을 감독한다.
|
||||
//
|
||||
// 앞의 넷은 자원 모델이었다. 여기는 **실패 위상**이다 — 자식이 죽었을 때
|
||||
// 그 사실이 어디까지 가는가, 누가 멈출 수 있는가.
|
||||
//
|
||||
// 압박하려는 주장 셋:
|
||||
// 1. spawn은 하나의 개념이고 실패가 어디까지 가느냐가 권한으로 갈린다
|
||||
// 2. 재시작 가능한 자식은 copyable 클로저여야 한다 — affinity가 검사한다
|
||||
// 3. 격리 경계는 recover가 아니다 — 죽었다는 사실만 넘어온다
|
||||
|
||||
import "cool.dev/std/list" as List
|
||||
import "cool.dev/std/string" as String
|
||||
import "cool.dev/std/int" as Int
|
||||
import "../std-draft/sup" as Sup
|
||||
import "../std-draft/db" as Db
|
||||
|
||||
// 로그를 쓸 권한. 감독자가 사람에게 보고하는 유일한 통로다 —
|
||||
// 아래 "spawn은 값을 돌려주지 않는다" 참고.
|
||||
pub capability Log {
|
||||
fn write(s: String) effects {Log.write}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 1. 재시작 가능한 일꾼
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
// 일감 하나를 처리한다. 자원을 **안에서** 얻고 안에서 반납한다.
|
||||
//
|
||||
// 이것이 재시작 가능성의 조건이다. 풀을 빌려 받을 뿐 lease를 들고 오지
|
||||
// 않으므로, 이 함수를 감싼 클로저는 affine이 아니다. 죽어도 다음 번에
|
||||
// 다시 획득하면 되고, 죽은 판의 lease는 감독자가 아니라 풀이 거둔다.
|
||||
pub fn handle(pool: Db.Pool, log: Log, sql: String)
|
||||
effects {Pool.acquire, Lease.query, Lease.release, Log.write} {
|
||||
match pool.acquire() {
|
||||
Err(_) => log.write("자리를 못 얻었다"),
|
||||
Ok(l) => report(log, l, sql),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report(log: Log, own l: Db.Lease, sql: String)
|
||||
effects {Lease.query, Lease.release, Log.write} {
|
||||
let rows = l.query(sql)
|
||||
let _ = l.release()
|
||||
match rows {
|
||||
Err(_) => log.write("질의가 실패했다"),
|
||||
Ok(rs) => log.write(String.concat("행 ", Int.show(List.len(rs)))),
|
||||
}
|
||||
}
|
||||
|
||||
// 감독 아래 돌린다. body가 무표기 fn이므로 여러 번 돌 수 있다.
|
||||
//
|
||||
// pool과 log는 빌린 capability다. 빌린 값을 capture한 클로저는 use이고,
|
||||
// use는 소유를 가져가는 자리로 못 간다 — supervise의 body는 빌림이므로 간다.
|
||||
pub fn run_worker(sup: Sup.Supervisor, sc: TaskScope, pool: Db.Pool, log: Log, sql: String)
|
||||
effects {Supervisor.supervise, Pool.acquire, Lease.query,
|
||||
Lease.release, Log.write} -> Sup.Stopped {
|
||||
sup.supervise(sc, fn() { handle(pool, log, sql) }, Sup.Policy { retries: 3 })
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 2. 재시작할 수 없는 일꾼
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
// lease를 밖에서 받아 들고 간다. 그래서 이 클로저는 affine이고,
|
||||
// **supervise에 넘길 수 없다.** 한 번밖에 못 도는 것을 재시작할 수는 없다.
|
||||
//
|
||||
// 언어가 그 사실을 시그니처에서 막는다. 얼랭이 관례로 지키는 것을
|
||||
// affinity가 검사한다 — 이 파일이 그것을 실제로 확인하는 자리다.
|
||||
pub fn run_once(sup: Sup.Supervisor, sc: TaskScope, log: Log, own l: Db.Lease, sql: String)
|
||||
effects {Supervisor.once, Lease.query, Lease.release, Log.write}
|
||||
-> Sup.Stopped {
|
||||
sup.once(sc, fn() { report(log, l, sql) })
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 3. 감독자가 보고한다
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
// 왜 멈췄는지 사람에게 알린다.
|
||||
//
|
||||
// Death에서 꺼낼 수 있는 것이 메시지뿐이다. 죽은 일꾼이 계산하던 값도,
|
||||
// 들고 있던 자원도 여기 없다 — 격리 경계가 recover가 아니라는 것의
|
||||
// 코드 수준 의미다. 담을 자리가 있으면 그것이 recover다.
|
||||
pub fn announce(log: Log, s: Sup.Stopped) effects {Log.write} {
|
||||
match s {
|
||||
Sup.Done => log.write("끝났다"),
|
||||
Sup.GaveUp(d) => match d {
|
||||
Sup.Crashed(msg) => log.write(String.concat("포기: ", msg)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 4. 전부 엮기
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
// 일감마다 일꾼 하나. 블록을 나갈 때 전원이 join된다.
|
||||
//
|
||||
// ※ 발견: 여기서 각 일꾼이 어떻게 끝났는지 알 수 없다. spawn한 클로저의
|
||||
// 반환값이 갈 데가 없기 때문이다. 그래서 보고를 값이 아니라 Log로 한다.
|
||||
pub fn supervise_all(
|
||||
sup: Sup.Supervisor,
|
||||
root: TaskScope,
|
||||
pool: Db.Pool,
|
||||
log: Log,
|
||||
jobs: List[String],
|
||||
) effects {TaskScope.spawn, Supervisor.supervise, Pool.acquire,
|
||||
Lease.query, Lease.release, Log.write} {
|
||||
scope work = root {
|
||||
List.each(jobs, fn(sql) {
|
||||
work.spawn(fn() {
|
||||
announce(log, run_worker(sup, work, pool, log, sql))
|
||||
})
|
||||
})
|
||||
}
|
||||
log.write("전원 종료")
|
||||
}
|
||||
+51
-9
@@ -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 =
|
||||
@@ -63,7 +65,14 @@ type expr =
|
||||
| E_lit of lit * pos
|
||||
| E_ident of string * pos
|
||||
| E_list of expr list * pos
|
||||
| E_struct of { name : string; fields : (string * expr) list; pos : pos }
|
||||
(* modl은 가져온 모듈의 별칭이다. 이것이 없으면 다른 모듈의 struct를
|
||||
만들 수 없다 — 타입은 쓸 수 있는데 값은 못 만드는 상태가 된다. *)
|
||||
| E_struct of {
|
||||
modl : string option;
|
||||
name : string;
|
||||
fields : (string * expr) list;
|
||||
pos : pos;
|
||||
}
|
||||
| E_closure of closure
|
||||
| E_if of { cond : expr; then_ : block; else_ : expr option; pos : pos }
|
||||
| E_match of { scrutinee : expr; arms : arm list; pos : pos }
|
||||
@@ -74,16 +83,21 @@ type expr =
|
||||
| E_inst of { callee : expr; args : targ list; pos : pos }
|
||||
| E_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 }
|
||||
|
||||
@@ -112,6 +126,14 @@ type param = {
|
||||
type fn_decl = {
|
||||
fn_name : string;
|
||||
fn_gen : gen_param list;
|
||||
(* 수신자를 소비하는가. capability 메서드에만 있고, 소스에는 `own self`로
|
||||
적는다. 빌리는 것이 기본이므로 빌림에는 적을 말이 없다 — 뜻이 있는
|
||||
쪽에만 표기가 있다.
|
||||
|
||||
이것이 없으면 자원이 자기 정리를 스스로 선언할 수 없고, 정리에 항상
|
||||
바깥 권한(Fs, Db)이 따라붙는다. 그러면 `with`가 자원만 보고 무엇을
|
||||
불러야 할지 알 수 없다 (dogfoods/FINDINGS D9). *)
|
||||
fn_self : bool;
|
||||
fn_params : param list;
|
||||
fn_eff : eff_result option;
|
||||
fn_ret : ty option;
|
||||
@@ -148,6 +170,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 +214,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 +273,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";
|
||||
@@ -253,8 +286,9 @@ let rec buf_expr b = function
|
||||
buf_expr b e)
|
||||
xs;
|
||||
Buffer.add_char b ')'
|
||||
| E_struct { name; fields; _ } ->
|
||||
Buffer.add_string b ("(struct " ^ name);
|
||||
| E_struct { modl; name; fields; _ } ->
|
||||
Buffer.add_string b
|
||||
("(struct " ^ (match modl with Some a -> a ^ "." | None -> "") ^ name);
|
||||
List.iter
|
||||
(fun (n, e) ->
|
||||
Buffer.add_string b (" (" ^ n ^ " ");
|
||||
@@ -265,9 +299,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 ':';
|
||||
@@ -391,6 +426,9 @@ let buf_gen b gen =
|
||||
let buf_fn b (d : fn_decl) =
|
||||
Buffer.add_string b ("(fn " ^ d.fn_name);
|
||||
buf_gen b d.fn_gen;
|
||||
(* 해시에 들어간다. 수신자를 소비하는지가 바뀌면 호출하는 쪽의 move 검사
|
||||
결과가 바뀐다 — 표면이다 *)
|
||||
if d.fn_self then Buffer.add_string b " own-self";
|
||||
Buffer.add_string b " (";
|
||||
buf_list b
|
||||
(fun p ->
|
||||
@@ -468,6 +506,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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
+222
-8
@@ -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,124 @@ 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.rest";
|
||||
"list.push";
|
||||
"list.concat";
|
||||
"list.reverse";
|
||||
"list.enumerate";
|
||||
"list.each";
|
||||
"list.map";
|
||||
"list.filter";
|
||||
"list.fold";
|
||||
"option.is_some";
|
||||
"option.map";
|
||||
"option.unwrap_or";
|
||||
"option.ok_or";
|
||||
"result.is_ok";
|
||||
"result.map";
|
||||
"result.map_err";
|
||||
"result.unwrap_or";
|
||||
]
|
||||
|
||||
let builtin pos name (args : value list) : value =
|
||||
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.rest", [ VList xs ] -> (
|
||||
match xs with [] -> VList [] | _ :: r -> VList r)
|
||||
| "list.first", [ VList xs ] -> (
|
||||
match xs with
|
||||
| [] -> VEnum ("Option", "None", [])
|
||||
| x :: _ -> VEnum ("Option", "Some", [ x ]))
|
||||
| "list.enumerate", [ VList xs ] ->
|
||||
VList
|
||||
(List.mapi
|
||||
(fun i x ->
|
||||
VStruct ("List.Indexed", [ ("i", ref (VInt i)); ("value", ref x) ]))
|
||||
xs)
|
||||
| "list.nth", [ VList xs; VInt i ] -> (
|
||||
match List.nth_opt xs i with
|
||||
| Some x -> VEnum ("Option", "Some", [ x ])
|
||||
| None -> VEnum ("Option", "None", []))
|
||||
| "string.join", [ VStr sep; VList parts ] ->
|
||||
VStr
|
||||
(String.concat sep
|
||||
(List.map (function VStr s -> s | v -> show v) parts))
|
||||
| "option.is_some", [ VEnum ("Option", v, _) ] -> VBool (v = "Some")
|
||||
| "option.unwrap_or", [ VEnum ("Option", "Some", [ x ]); _ ] -> x
|
||||
| "option.unwrap_or", [ _; fallback ] -> fallback
|
||||
| "option.ok_or", [ VEnum ("Option", "Some", [ x ]); _ ] ->
|
||||
VEnum ("Result", "Ok", [ x ])
|
||||
| "option.ok_or", [ _; e ] -> VEnum ("Result", "Err", [ e ])
|
||||
| "result.is_ok", [ VEnum ("Result", v, _) ] -> VBool (v = "Ok")
|
||||
| "result.unwrap_or", [ VEnum ("Result", "Ok", [ x ]); _ ] -> x
|
||||
| "result.unwrap_or", [ _; fallback ] -> fallback
|
||||
| _ ->
|
||||
fail pos (Printf.sprintf "%s은(는) 런타임이 제공하지 않습니다 (표준 라이브러리가 아직 없습니다)" name)
|
||||
|
||||
@@ -181,6 +323,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 +372,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 +489,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 +511,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 })
|
||||
|
||||
@@ -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,19 +186,21 @@ 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)
|
||||
| Ast.E_struct { name; fields; _ } ->
|
||||
I_make (name, List.map (fun (n, e) -> (n, lower c e)) fields)
|
||||
| Ast.E_struct { modl; name; fields; _ } ->
|
||||
let key = match modl with Some a -> a ^ "." ^ name | None -> name in
|
||||
I_make (key, List.map (fun (n, e) -> (n, lower c e)) fields)
|
||||
| Ast.E_closure cl ->
|
||||
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 +315,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 +383,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;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
(* 문법 문서의 어휘 절을 코드에서 생성한다.
|
||||
|
||||
키워드 표와 "줄을 끝낼 수 있는 토큰" 목록은 지금까지 token.ml과
|
||||
grammar.ebnf 양쪽에 손으로 적혀 있었다. 그런 목록은 어긋난다 —
|
||||
실제로 이 프로젝트의 문법 문서 전체가 그렇게 어긋났다.
|
||||
|
||||
여기서 생성하고 테스트가 대조하므로, 이제 토큰을 추가하면 문서가 낡거나
|
||||
빌드가 깨진다. 사람이 두 곳을 맞출 의무가 없다. *)
|
||||
|
||||
let display (k : Token.kind) =
|
||||
match k with
|
||||
| Token.Ident _ -> Some "ident"
|
||||
| Token.Int _ -> Some "int_lit"
|
||||
| Token.Str _ -> Some "string_lit"
|
||||
| Token.Newline | Token.Eof -> None
|
||||
| _ -> Some ("\"" ^ Token.show_kind k ^ "\"")
|
||||
|
||||
let is_keyword (k : Token.kind) = Token.keyword (Token.show_kind k) = Some k
|
||||
|
||||
let keywords () =
|
||||
List.filter_map
|
||||
(fun k ->
|
||||
if is_keyword k then Some ("\"" ^ Token.show_kind k ^ "\"") else None)
|
||||
Token.all_kinds
|
||||
|
||||
let statement_enders () =
|
||||
List.filter_map
|
||||
(fun k -> if Token.can_end_statement k then display k else None)
|
||||
Token.all_kinds
|
||||
|
||||
let begin_mark = "(* 여기부터 lib/token.ml에서 생성됩니다 — 손으로 고치지 마십시오 *)"
|
||||
|
||||
let end_mark = "(* 생성 끝 *)"
|
||||
|
||||
(* 한 줄이 길어지지 않게 접는다. 구분자는 줄 끝에 남겨 이어짐이 보이게 한다 *)
|
||||
let wrap ~indent items =
|
||||
let rec go acc line = function
|
||||
| [] -> List.rev (line :: acc)
|
||||
| x :: rest ->
|
||||
if line = indent then go acc (line ^ x) rest
|
||||
else if String.length line + String.length x + 3 > 72 then
|
||||
go ((line ^ " |") :: acc) (indent ^ x) rest
|
||||
else go acc (line ^ " | " ^ x) rest
|
||||
in
|
||||
String.concat "\n" (go [] indent items)
|
||||
|
||||
let render () =
|
||||
String.concat "\n"
|
||||
[
|
||||
begin_mark;
|
||||
" * 줄을 끝낼 수 있는 토큰. 줄의 마지막 토큰이 이 중 하나이면 그 줄 끝에";
|
||||
" * NEWLINE이 삽입된다. Token.can_end_statement가 원본이다.";
|
||||
" *";
|
||||
wrap ~indent:" * " (statement_enders ());
|
||||
" *";
|
||||
" * 키워드. 이름으로 쓸 수 없다. Token.keyword가 원본이다.";
|
||||
" *";
|
||||
wrap ~indent:" * " (keywords ());
|
||||
" " ^ end_mark;
|
||||
]
|
||||
+246
-43
@@ -13,7 +13,9 @@
|
||||
|
||||
open Ast
|
||||
|
||||
type error = { pos : Token.pos; msg : string }
|
||||
(* lint는 오류가 아니다. 코드는 뜻이 통하고 검사도 계속되어야 한다 —
|
||||
Resolve와 같은 구분이다 *)
|
||||
type error = { pos : Token.pos; msg : string; lint : bool }
|
||||
|
||||
(* 값이 무엇인가: 소유한 affine 값인가, 빌린 값인가. 둘은 직교한다. *)
|
||||
type vinfo = { v_affine : bool; v_use : bool }
|
||||
@@ -27,13 +29,23 @@ 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 fninfo = { f_self : bool; 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;
|
||||
(* 열거형 생성자 이름. 생성자는 인자를 소유한다 — 값이 variant 안으로
|
||||
들어가기 때문이다. 이것이 없으면 Some(f)를 두 번 써서 affine 값을
|
||||
복제할 수 있다 (보안 정리 ii). 리스트 리터럴은 처음부터 막혀 있었고
|
||||
struct 리터럴도 그랬는데, 생성자만 호출로 취급돼 빠져 있었다. *)
|
||||
ctors : (string, unit) Hashtbl.t;
|
||||
meths : (string, (string * fninfo) list) Hashtbl.t;
|
||||
mutable scopes : binding list list;
|
||||
moved : (int, Token.pos) Hashtbl.t;
|
||||
@@ -41,10 +53,15 @@ 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;
|
||||
}
|
||||
|
||||
let err st pos msg = st.errors <- { pos; msg } :: st.errors
|
||||
let err st pos msg = st.errors <- { pos; msg; lint = false } :: st.errors
|
||||
let lint st pos msg = st.errors <- { pos; msg; lint = true } :: st.errors
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
(* affinity 유도 *)
|
||||
@@ -74,7 +91,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 +142,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 +152,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 +205,50 @@ 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
|
||||
|
||||
(* 지금 살아 있는, 소유한 affine 바인딩. 소유했다(use가 아니다)는 것은
|
||||
정리할 책임이 이 함수에 있다는 뜻이고, moved가 아니라는 것은 아직
|
||||
아무 데도 넘기지 않았다는 뜻이다. *)
|
||||
let live_owned st =
|
||||
List.concat_map
|
||||
(fun scope ->
|
||||
List.filter
|
||||
(fun b -> b.b_affine && (not b.b_use) && not (Hashtbl.mem st.moved b.b_id))
|
||||
scope)
|
||||
st.scopes
|
||||
|
||||
(* 조기 탈출 지점에서 새는 것을 말한다. 이름순으로 하나의 메시지에 모은다 —
|
||||
자원 셋을 들고 있으면 같은 자리에 경고가 셋 뜨는 것이 아니라 하나 뜬다. *)
|
||||
let live_leaks st pos what =
|
||||
match List.sort compare (List.map (fun b -> b.b_name) (live_owned st)) with
|
||||
| [] -> ()
|
||||
| names ->
|
||||
lint st pos
|
||||
(Printf.sprintf "%s이(가) 여기서 빠져나가면 %s이(가) 정리되지 않습니다 (%s는 뒤의 정리를 건너뜁니다)"
|
||||
what (String.concat ", " names) what)
|
||||
|
||||
let rec walk st (ctx : ctx) (e : expr) : vinfo =
|
||||
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
|
||||
@@ -210,11 +272,12 @@ let rec walk st (ctx : ctx) (e : expr) : vinfo =
|
||||
| E_list (xs, _) ->
|
||||
let infos = List.map (walk st (Move "컨테이너에 넣을 수")) xs in
|
||||
{ v_affine = List.exists (fun i -> i.v_affine) infos; v_use = false }
|
||||
| E_struct { name; fields; _ } ->
|
||||
| E_struct { modl; name; fields; _ } ->
|
||||
List.iter (fun (_, e) -> ignore (walk st (Move "struct에 저장할 수") e)) fields;
|
||||
let key = match modl with Some a -> a ^ "." ^ name | None -> name in
|
||||
{
|
||||
v_affine =
|
||||
(match Hashtbl.find_opt st.aff name with
|
||||
(match Hashtbl.find_opt st.aff key with
|
||||
| Some b -> b
|
||||
| None -> false);
|
||||
v_use = false;
|
||||
@@ -243,6 +306,25 @@ let rec walk st (ctx : ctx) (e : expr) : vinfo =
|
||||
{ v_affine = t1.v_affine || t2.v_affine; v_use = t1.v_use || t2.v_use }
|
||||
| E_match { scrutinee; arms; _ } ->
|
||||
let sinfo = walk st Borrow scrutinee in
|
||||
(* 소유한 affine 값을 이름을 묶는 패턴으로 분해하면 그 값 자체가
|
||||
소비된다. 조각이 밖으로 나가기 때문이다 — 그러지 않으면 같은
|
||||
파이프를 두 번 분해해 같은 fd를 두 번 꺼낼 수 있다 (보안 정리 ii).
|
||||
|
||||
빌린 값은 여기서 건드리지 않는다. 빌린 scrutinee의 조각은 use로
|
||||
묶이므로 꺼내려는 순간 각자의 자리에서 걸리고, 그쪽 진단이 더
|
||||
정확하다. 필드에서 분해하는 것도 마찬가지로 E_field가 잡는다. *)
|
||||
(if
|
||||
sinfo.v_affine && (not sinfo.v_use)
|
||||
&& List.exists (fun a -> pat_binds a.arm_pat) arms
|
||||
then
|
||||
match scrutinee with
|
||||
| E_ident (n, spos) -> (
|
||||
match find st n with
|
||||
| Some b when b.b_affine && not b.b_use ->
|
||||
if not (Hashtbl.mem st.moved b.b_id) then
|
||||
Hashtbl.replace st.moved b.b_id spos
|
||||
| _ -> ())
|
||||
| _ -> ());
|
||||
let before = snapshot st in
|
||||
let results =
|
||||
List.map
|
||||
@@ -264,7 +346,7 @@ let rec walk st (ctx : ctx) (e : expr) : vinfo =
|
||||
ignore (walk st Borrow (E_ident (parent, Token.{ line = 0; col = 0 })));
|
||||
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,24 +356,55 @@ 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_try { inner; pos } ->
|
||||
let r = walk st ctx inner in
|
||||
(* ?는 여기서 함수를 빠져나갈 수 있다. 그 길에는 뒤에 오는 정리
|
||||
코드가 없다. 지금 살아 있는, 소유한 affine 값은 그 길에서 샌다.
|
||||
|
||||
v0 정책은 "오용 금지, 누수 허용"이므로 이것은 오류가 아니라
|
||||
lint다 — 검사를 막지 않고 사실만 말한다. 누수를 언어가 강제로
|
||||
막을지는 `with`가 결정할 문제이고, 그 결정에는 "실제 코드에서
|
||||
얼마나 자주 새는가"라는 측정이 필요하다. 이 lint가 그 측정이다. *)
|
||||
live_leaks st pos "?";
|
||||
r
|
||||
| E_unary { operand; _ } -> walk st Borrow operand
|
||||
| E_binary { lhs; rhs; _ } ->
|
||||
ignore (walk st Borrow lhs);
|
||||
ignore (walk st Borrow rhs);
|
||||
v_copy
|
||||
|
||||
(* 이름을 묶는 자리가 하나라도 있는가. 있으면 분해로 값이 밖으로 나간다 *)
|
||||
and pat_binds = function
|
||||
| P_bind _ -> true
|
||||
| P_ctor { args; _ } -> List.exists pat_binds args
|
||||
| P_wild _ | P_lit _ -> false
|
||||
|
||||
and bind_pattern st (info : vinfo) p =
|
||||
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,15 +423,16 @@ 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
|
||||
bind pat
|
||||
| S_return { value; _ } -> (
|
||||
match value with
|
||||
| S_return { value; pos } ->
|
||||
(match value with
|
||||
| None -> ()
|
||||
| Some e -> ignore (walk st (Move "반환할 수") e))
|
||||
| Some e -> ignore (walk st (Move "반환할 수") e));
|
||||
live_leaks st pos "return"
|
||||
| S_assign { place; value; _ } ->
|
||||
ignore (walk st (Move "대입할 수") value);
|
||||
ignore (walk st Borrow place)
|
||||
@@ -330,10 +444,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;
|
||||
@@ -358,9 +486,35 @@ and walk_closure st (c : closure) : vinfo =
|
||||
captured;
|
||||
{ v_affine = !affine; v_use = !use_ }
|
||||
|
||||
and is_ctor st callee =
|
||||
match callee with
|
||||
| E_ident (n, _) when find st n = None -> Hashtbl.mem st.ctors n
|
||||
(* Alias.Variant — 가져온 모듈의 생성자 *)
|
||||
| E_field { obj = E_ident (o, _); name; _ } when find st o = None ->
|
||||
Hashtbl.mem st.ctors name || Hashtbl.mem st.ctors (o ^ "." ^ name)
|
||||
| _ -> false
|
||||
|
||||
and walk_call st callee args pos =
|
||||
let info = callee_info st callee in
|
||||
ignore (walk st Borrow callee);
|
||||
if info = None && is_ctor st callee then begin
|
||||
(* 생성자는 인자를 소유한다. 값이 variant 안으로 들어가기 때문이다 —
|
||||
리스트 리터럴, struct 리터럴과 같은 규칙이다.
|
||||
|
||||
affinity는 넣은 것에서 전이된다. 선언된 타입이 아니라 실제로 넣은
|
||||
값을 보는 것은 E_list와 같다. *)
|
||||
let infos = List.map (walk st (Move "열거형에 넣을 수")) args in
|
||||
{ v_affine = List.exists (fun i -> i.v_affine) infos; v_use = false }
|
||||
end
|
||||
else walk_plain_call st callee args pos info
|
||||
|
||||
and walk_plain_call st callee args pos info =
|
||||
(* 수신자를 소비하는 메서드면 수신자가 Move 자리다. 이것이 없으면
|
||||
f.close()를 두 번 부를 수 있다 — 그리고 자원이 자기 정리를 스스로
|
||||
선언한다는 말 자체가 성립하지 않는다. *)
|
||||
(match (callee, info) with
|
||||
| E_field { obj; name; _ }, Some { f_self = true; _ } ->
|
||||
ignore (walk st (Move (Printf.sprintf "%s()로 소비할 수" name)) obj)
|
||||
| _ -> ignore (walk st Borrow callee));
|
||||
let params = match info with Some f -> f.f_params | None -> [] in
|
||||
List.iteri
|
||||
(fun i a ->
|
||||
@@ -368,6 +522,9 @@ and walk_call st callee args pos =
|
||||
let own = match p with Some p -> p.p_own | None -> false in
|
||||
let 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
|
||||
@@ -385,24 +542,42 @@ and callee_info st callee =
|
||||
| E_ident (n, _) when find st n = None -> Hashtbl.find_opt st.fns n
|
||||
| E_inst { callee = E_ident (n, _); _ } when find st n = None ->
|
||||
Hashtbl.find_opt st.fns n
|
||||
| E_field { obj = E_ident (o, _); name; _ } -> (
|
||||
match find st o with
|
||||
| Some _ -> (
|
||||
(* 값의 메서드: 타입을 모르면 넘어간다. capability 메서드는 아래에서 *)
|
||||
match
|
||||
Hashtbl.fold
|
||||
(fun _ methods acc ->
|
||||
match acc with
|
||||
| Some _ -> acc
|
||||
| None -> List.assoc_opt name methods)
|
||||
st.meths None
|
||||
with
|
||||
| Some f -> Some f
|
||||
| None -> None)
|
||||
(* 모듈 별칭을 통한 호출: 가져온 함수는 "Alias.f" 키로 들어와 있다 *)
|
||||
| None -> Hashtbl.find_opt st.fns (o ^ "." ^ name))
|
||||
(* 모듈 별칭을 통한 호출: 가져온 함수는 "Alias.f" 키로 들어와 있다 *)
|
||||
| E_field { obj = E_ident (o, _); name; _ } when find st o = None ->
|
||||
Hashtbl.find_opt st.fns (o ^ "." ^ name)
|
||||
(* 값의 메서드. 수신자가 중첩된 필드여도(w.pay.close()) 여기로 온다 —
|
||||
예전에는 obj가 이름일 때만 봤고, 그래서 중첩 수신자는 시그니처를 못
|
||||
찾아 조용히 빌림이 됐다. *)
|
||||
| E_field { obj; name; _ } -> meth_info st obj name
|
||||
| _ -> None
|
||||
|
||||
(* 수신자의 메서드 시그니처를 찾는다.
|
||||
|
||||
수신자의 선언된 타입을 알면 그 capability 안에서만 찾는다. 모르면 이름만
|
||||
보고 아무 capability에서나 찾는다 — 이름이 겹치면 엉뚱한 시그니처를 읽고
|
||||
진단이 부정확해질 수 있다. 통과가 느슨해지지는 않는다 (타입 검사가 수신자
|
||||
타입을 이미 확인한다). *)
|
||||
and meth_info st obj name =
|
||||
let by_name () =
|
||||
Hashtbl.fold
|
||||
(fun _ methods acc ->
|
||||
match acc with Some _ -> acc | None -> List.assoc_opt name methods)
|
||||
st.meths None
|
||||
in
|
||||
let ty_name : ty option =
|
||||
match obj with
|
||||
| E_ident (n, _) -> Option.bind (find st n) (fun b -> b.b_ty)
|
||||
| E_field { obj; name; _ } -> field_ty st obj name
|
||||
| _ -> None
|
||||
in
|
||||
match ty_name with
|
||||
| Some (T_named { modl; name = tn; _ }) -> (
|
||||
let key = match modl with Some a -> a ^ "." ^ tn | None -> tn in
|
||||
match Hashtbl.find_opt st.meths key with
|
||||
| Some methods -> List.assoc_opt name methods
|
||||
| None -> by_name ())
|
||||
| _ -> by_name ()
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
(* 선언 *)
|
||||
(* ------------------------------------------------------------------ *)
|
||||
@@ -421,7 +596,7 @@ let check_fn st (d : fn_decl) =
|
||||
let affine = ty_affine st p.p_ty in
|
||||
(* 무표기 = 빌림. 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,33 +612,61 @@ let check ?(imports : item list = []) (m : modul) : error list =
|
||||
let st =
|
||||
{
|
||||
aff = Hashtbl.create 16;
|
||||
fields = Hashtbl.create 16;
|
||||
fns = Hashtbl.create 16;
|
||||
ctors = Hashtbl.create 16;
|
||||
meths = Hashtbl.create 16;
|
||||
scopes = [];
|
||||
moved = Hashtbl.create 16;
|
||||
next_id = 0;
|
||||
depth = 0;
|
||||
frames = [];
|
||||
cl_expect = None;
|
||||
errors = [];
|
||||
}
|
||||
in
|
||||
derive_affinity st (imports @ m.items);
|
||||
(* 내장 열거형의 생성자. 이름만 필요하다 *)
|
||||
List.iter
|
||||
(fun n -> Hashtbl.replace st.ctors n ())
|
||||
[ "Some"; "None"; "Ok"; "Err" ];
|
||||
List.iter
|
||||
(fun it ->
|
||||
match it with
|
||||
| I_enum { variants; _ } ->
|
||||
List.iter (fun v -> Hashtbl.replace st.ctors v.v_name ()) variants
|
||||
| _ -> ())
|
||||
(imports @ m.items);
|
||||
List.iter
|
||||
(fun it ->
|
||||
match it with
|
||||
| I_fn { decl; _ } ->
|
||||
Hashtbl.replace st.fns decl.fn_name
|
||||
{ f_params = decl.fn_params; f_ret = decl.fn_ret }
|
||||
{ f_self = false; f_params = decl.fn_params; f_ret = decl.fn_ret }
|
||||
| I_capability { name; methods; _ } ->
|
||||
Hashtbl.replace st.meths name
|
||||
(List.map
|
||||
(fun d ->
|
||||
(d.fn_name, { f_params = d.fn_params; f_ret = d.fn_ret }))
|
||||
( d.fn_name,
|
||||
{
|
||||
f_self = d.fn_self;
|
||||
f_params = d.fn_params;
|
||||
f_ret = d.fn_ret;
|
||||
} ))
|
||||
methods)
|
||||
| _ -> ())
|
||||
(imports @ m.items);
|
||||
List.iter
|
||||
(fun it -> match it with I_fn { decl; _ } -> check_fn st decl | _ -> ())
|
||||
(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 ->
|
||||
|
||||
+97
-38
@@ -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
|
||||
@@ -405,11 +412,22 @@ and parse_primary st =
|
||||
let parent = ident st "부모 scope 이름" in
|
||||
let body = parse_block st in
|
||||
E_scope { name; parent; body; pos = p }
|
||||
(* name_or_struct<s>. struct를 만들 수 있는 문맥에서만 점을 미리 먹는다 —
|
||||
Alias.Name { ... }의 "{"를 받을 자리가 여기밖에 없기 때문이다.
|
||||
<no>에서는 안 먹으므로 if x.y { } 의 "{"는 언제나 블록이다. *)
|
||||
| Token.Ident n ->
|
||||
adv st;
|
||||
if (not st.no_struct) && kind st = Token.LBrace then
|
||||
if (not st.no_struct) && kind st = Token.Dot then begin
|
||||
adv st;
|
||||
let m = ident st "이름" in
|
||||
if kind st = Token.LBrace then
|
||||
let fields = parse_struct_lit_fields st in
|
||||
E_struct { modl = Some n; name = m; fields; pos = p }
|
||||
else E_field { obj = E_ident (n, p); name = m; pos = p }
|
||||
end
|
||||
else if (not st.no_struct) && kind st = Token.LBrace then
|
||||
let fields = parse_struct_lit_fields st in
|
||||
E_struct { name = n; fields; pos = p }
|
||||
E_struct { modl = None; name = n; fields; pos = p }
|
||||
else E_ident (n, p)
|
||||
| _ -> err_expect st "식"
|
||||
|
||||
@@ -443,12 +461,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 +584,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
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
@@ -600,36 +615,69 @@ let parse_gen_params st =
|
||||
expect_close st Token.RBracket "]";
|
||||
gs
|
||||
|
||||
let parse_params st =
|
||||
if kind st = Token.RParen then []
|
||||
(* params = list<param>
|
||||
param = [ "own" ] , plain_param
|
||||
plain_param = [ "mut" ] , ident , ":" , type
|
||||
|
||||
capability 메서드에서만 첫 자리가 다르다 (좌인수분해 — own을 먼저 먹고
|
||||
그 다음을 보므로 선읽기는 여전히 한 토큰이다):
|
||||
|
||||
cap_params = "own" , cap_own_rest | plain_param , list_rest<param>
|
||||
cap_own_rest = "self" , list_rest<param> | plain_param , list_rest<param>
|
||||
|
||||
일반 함수에는 `own self`를 적을 자리가 문법에 없다 — 검사기가 거부하는
|
||||
것이 아니라 그 문장이 존재하지 않는다.
|
||||
|
||||
돌려주는 bool은 수신자를 소비하는가다. self는 fn_params에 넣지 않는다 —
|
||||
호출 지점에 인자로 나타나지 않으므로 파라미터가 아니다. *)
|
||||
let parse_params st ~allow_self : bool * param list =
|
||||
(* [ "mut" ] ident ":" type — own을 이미 먹었을 수 있으므로 밖에서 받는다 *)
|
||||
let plain_param st ~own ~p =
|
||||
let mut_ = accept st Token.Kw_mut in
|
||||
let name = ident st "파라미터 이름" in
|
||||
expect st Token.Colon ":";
|
||||
let ty = parse_ty st in
|
||||
{ p_own = own; p_mut = mut_; p_name = name; p_ty = ty; p_pos = p }
|
||||
in
|
||||
(* list_rest<param>: 후행 콤마를 허용한다 *)
|
||||
let rec rest acc =
|
||||
if accept st Token.Comma then
|
||||
if kind st = Token.RParen then List.rev acc
|
||||
else
|
||||
let p = pos st in
|
||||
let own = accept st Token.Kw_own in
|
||||
(* self는 첫 자리에만 온다. 뒤에 나오면 수신자가 아니다 *)
|
||||
if kind st = Token.Kw_self then
|
||||
err st "self는 capability 메서드의 첫 파라미터로만 올 수 있습니다";
|
||||
rest (plain_param st ~own ~p :: acc)
|
||||
else List.rev acc
|
||||
in
|
||||
if kind st = Token.RParen then (false, [])
|
||||
else
|
||||
let rec loop acc =
|
||||
let p = pos st in
|
||||
let own = accept st Token.Kw_own in
|
||||
let mut_ = accept st Token.Kw_mut in
|
||||
let name = ident st "파라미터 이름" in
|
||||
expect st Token.Colon ":";
|
||||
let ty = parse_ty st in
|
||||
let prm =
|
||||
{ p_own = own; p_mut = mut_; p_name = name; p_ty = ty; p_pos = p }
|
||||
in
|
||||
if accept st Token.Comma then
|
||||
if kind st = Token.RParen then List.rev (prm :: acc)
|
||||
else loop (prm :: acc)
|
||||
else List.rev (prm :: acc)
|
||||
in
|
||||
loop []
|
||||
let p = pos st in
|
||||
if accept st Token.Kw_own then
|
||||
if allow_self && accept st Token.Kw_self then (true, rest [])
|
||||
else (
|
||||
if kind st = Token.Kw_self then
|
||||
err st "self는 capability 메서드에서만 쓸 수 있습니다";
|
||||
(false, rest [ plain_param st ~own:true ~p ]))
|
||||
else (
|
||||
(* own 없는 self. 빌리는 것이 기본이라 적을 말이 없다 —
|
||||
같은 뜻에 두 가지 적는 법을 만들지 않는다 (철학 5) *)
|
||||
if kind st = Token.Kw_self then
|
||||
err st "self는 own self로만 적습니다 (수신자를 빌리는 것이 기본이라 적을 말이 없습니다)";
|
||||
(false, rest [ plain_param st ~own:false ~p ]))
|
||||
|
||||
(* fn_decl = "fn" ident [gen] "(" [params] ")" {NL}
|
||||
[eff_result {NL}] ["->" type {NL}] [block]
|
||||
{ NEWLINE } 흡수 위치는 문법에 적힌 그대로다. *)
|
||||
let parse_fn_decl st ~allow_body =
|
||||
let parse_fn_decl st ~allow_body ~allow_self =
|
||||
let p = pos st in
|
||||
expect st Token.Kw_fn "fn";
|
||||
let name = ident st "함수 이름" in
|
||||
let gen = if kind st = Token.LBracket then parse_gen_params st else [] in
|
||||
expect st Token.LParen "(";
|
||||
let params = parse_params st in
|
||||
let self_, params = parse_params st ~allow_self in
|
||||
expect_close st Token.RParen ")";
|
||||
skip_nl st;
|
||||
let eff =
|
||||
@@ -652,6 +700,7 @@ let parse_fn_decl st ~allow_body =
|
||||
{
|
||||
fn_name = name;
|
||||
fn_gen = gen;
|
||||
fn_self = self_;
|
||||
fn_params = params;
|
||||
fn_eff = eff;
|
||||
fn_ret = ret;
|
||||
@@ -737,7 +786,7 @@ let parse_capability st ~pub =
|
||||
let rec loop acc =
|
||||
if kind st = Token.RBrace then List.rev acc
|
||||
else
|
||||
let m = parse_fn_decl st ~allow_body:false in
|
||||
let m = parse_fn_decl st ~allow_body:false ~allow_self:true in
|
||||
skip_nl st;
|
||||
loop (m :: acc)
|
||||
in
|
||||
@@ -764,10 +813,20 @@ 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
|
||||
| Token.Kw_fn -> I_fn { pub; decl = parse_fn_decl st ~allow_body:true }
|
||||
| Token.Kw_fn -> I_fn { pub; decl = parse_fn_decl st ~allow_body:true ~allow_self:false }
|
||||
| Token.Kw_copyable ->
|
||||
adv st;
|
||||
parse_struct st ~pub ~copyable:true
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
(* 문법 파일이 직접 읽는 인식기.
|
||||
|
||||
손으로 쓴 파서(lib/parser.ml)와 같은 토큰 열을 받아 같은 판정을 내야 한다.
|
||||
갈리면 둘 중 하나가 틀린 것이고, 그 순간 테스트가 깨진다. 이것이 설명서와
|
||||
구현이 어긋나지 않게 하는 기계적 장치다.
|
||||
|
||||
AST를 만들지 않는다. 받아들이는가만 답한다. 그래서 손 파서의 진단은
|
||||
그대로 남는다 — 대조는 판정만 한다.
|
||||
|
||||
문법이 LL(1)임이 이미 검증되었으므로 여기서 선택은 결정적이다.
|
||||
다음 토큰이 어느 대안의 FIRST에 있는지만 보면 되고 되돌아가지 않는다. *)
|
||||
|
||||
module SS = Set.Make (String)
|
||||
|
||||
type error = { pos : Token.pos; expected : string; got : string }
|
||||
|
||||
(* 토큰을 문법의 단말 이름으로 옮긴다. 이 대응이 문법과 렉서를 잇는 유일한
|
||||
지점이다 — 여기가 틀리면 대조 전체가 무의미하다. *)
|
||||
let terminal_of (k : Token.kind) =
|
||||
match k with
|
||||
| Token.Ident _ -> "ident"
|
||||
| Token.Int _ -> "int_lit"
|
||||
| Token.Str _ -> "string_lit"
|
||||
| Token.Newline -> "NEWLINE"
|
||||
| Token.Eof -> "<eof>"
|
||||
| k -> Token.show_kind k
|
||||
|
||||
type state = {
|
||||
toks : Token.t array;
|
||||
mutable i : int;
|
||||
rules : (string, Ebnf.rule) Hashtbl.t;
|
||||
a : Ebnf.analysis;
|
||||
tokens : SS.t;
|
||||
mutable err : error option;
|
||||
}
|
||||
|
||||
exception Fail
|
||||
|
||||
let cur st = st.toks.(st.i)
|
||||
let term st = terminal_of (cur st).Token.kind
|
||||
|
||||
let fail st expected =
|
||||
(* 가장 멀리 간 실패를 남긴다. 그 자리가 사람이 볼 자리다 *)
|
||||
let keep =
|
||||
match st.err with
|
||||
| None -> true
|
||||
| Some e ->
|
||||
(e.pos.Token.line, e.pos.Token.col)
|
||||
<= ((cur st).Token.pos.Token.line, (cur st).Token.pos.Token.col)
|
||||
in
|
||||
if keep then
|
||||
st.err <- Some { pos = (cur st).Token.pos; expected; got = term st };
|
||||
raise Fail
|
||||
|
||||
let advance st = if st.i < Array.length st.toks - 1 then st.i <- st.i + 1
|
||||
let is_tok st n = SS.mem n st.tokens || not (Hashtbl.mem st.rules n)
|
||||
|
||||
(* 이 식이 지금 토큰으로 시작할 수 있는가 *)
|
||||
let starts st e = SS.mem (term st) (Ebnf.first_expr st.a e)
|
||||
|
||||
let rec run st (e : Ebnf.expr) =
|
||||
match e with
|
||||
| Ebnf.Term s -> if term st = s then advance st else fail st ("\"" ^ s ^ "\"")
|
||||
| Ebnf.Ref n ->
|
||||
if is_tok st n then if term st = n then advance st else fail st n
|
||||
else run st (Hashtbl.find st.rules n).Ebnf.body
|
||||
| Ebnf.RefArg (n, x) -> run st (Ebnf.Ref (Ebnf.mangle n x))
|
||||
| Ebnf.Seq xs -> List.iter (run st) xs
|
||||
| Ebnf.Alt xs -> (
|
||||
match List.find_opt (starts st) xs with
|
||||
| Some x -> run st x
|
||||
| None -> (
|
||||
(* 비어도 되는 대안이 있으면 그것을 고른다 *)
|
||||
match List.find_opt (Ebnf.nullable_expr st.a) xs with
|
||||
| Some x -> run st x
|
||||
| None ->
|
||||
fail st
|
||||
(String.concat " 또는 " (SS.elements (Ebnf.first_expr st.a e)))))
|
||||
(* 선택과 반복은 최대한 먹는다 (문법 표기 규약의 greedy 규칙) *)
|
||||
| Ebnf.Opt x -> if starts st x then run st x
|
||||
| Ebnf.Rep x ->
|
||||
while starts st x do
|
||||
run st x
|
||||
done
|
||||
| Ebnf.Except (x, _) -> run st x
|
||||
|
||||
let check ?(tokens = []) ?(start = "module") (g : Ebnf.t) (toks : Token.t list)
|
||||
: (unit, error) result =
|
||||
let g = Ebnf.expand g in
|
||||
let rules = Hashtbl.create 128 in
|
||||
List.iter (fun (r : Ebnf.rule) -> Hashtbl.replace rules r.name r) g;
|
||||
let st =
|
||||
{
|
||||
toks = Array.of_list toks;
|
||||
i = 0;
|
||||
rules;
|
||||
a = Ebnf.analyze ~tokens g;
|
||||
tokens = SS.of_list tokens;
|
||||
err = None;
|
||||
}
|
||||
in
|
||||
match run st (Ebnf.Ref start) with
|
||||
| () ->
|
||||
if term st = "<eof>" then Ok ()
|
||||
else (
|
||||
(match st.err with
|
||||
| Some _ -> ()
|
||||
| None ->
|
||||
st.err <-
|
||||
Some
|
||||
{ pos = (cur st).Token.pos; expected = "파일 끝"; got = term st });
|
||||
Error (Option.get st.err))
|
||||
| exception Fail -> Error (Option.get st.err)
|
||||
+31
-11
@@ -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 ()
|
||||
@@ -163,15 +164,15 @@ let rec resolve_expr st = function
|
||||
(* 타입 이름은 그 타입에 딸린 함수의 이름공간이다: String.len *)
|
||||
else external_ref st n pos
|
||||
| E_list (xs, _) -> List.iter (resolve_expr st) xs
|
||||
| E_struct { name; fields; pos } ->
|
||||
resolve_ty st (T_named { modl = None; name; args = []; pos });
|
||||
| E_struct { modl; name; fields; pos } ->
|
||||
resolve_ty st (T_named { modl; name; args = []; pos });
|
||||
List.iter (fun (_, e) -> resolve_expr st e) fields
|
||||
| E_closure c ->
|
||||
push st;
|
||||
List.iter
|
||||
(fun (n, t) ->
|
||||
Option.iter (resolve_ty st) t;
|
||||
bind st c.cl_pos n false)
|
||||
(fun (p : cl_param) ->
|
||||
Option.iter (resolve_ty st) p.cp_ty;
|
||||
bind st c.cl_pos p.cp_name false)
|
||||
c.cl_params;
|
||||
(match c.cl_eff with
|
||||
| None -> ()
|
||||
@@ -211,10 +212,24 @@ let rec resolve_expr st = function
|
||||
| E_call { callee; args; _ } ->
|
||||
resolve_expr st callee;
|
||||
List.iter (resolve_expr st) args
|
||||
| E_field { obj; _ } ->
|
||||
(* Alias.f — 별칭 접근도 사용이다 *)
|
||||
| E_field { obj; pos; _ } ->
|
||||
(* Alias.f — 별칭 접근도 사용이다.
|
||||
|
||||
왼쪽이 지역 값도, 가져온 모듈도, 이 모듈의 선언도 아니면 그 이름은
|
||||
그냥 없는 것이다. 바깥 참조로 넘기면 안 된다 — 넘기는 순간 오른쪽이
|
||||
전부 불투명해지고 그 아래 검사가 통째로 공허해진다 (D1과 같은 병).
|
||||
|
||||
내장 타입 이름도 모듈이 아니다. 타입으로 쓰는 데는 import가 필요
|
||||
없지만 그 타입에 딸린 함수를 쓰려면 가져와야 한다 — prelude가
|
||||
없다는 규칙에 예외를 두지 않는다. *)
|
||||
(match obj with
|
||||
| E_ident (o, _) when lookup_local st o = None -> use_alias st o
|
||||
| E_ident (o, _) when lookup_local st o = None ->
|
||||
if Hashtbl.mem st.items o then use_alias st o
|
||||
else if List.mem o builtin_types then
|
||||
error st pos
|
||||
(Printf.sprintf "%s은(는) 내장 타입이지 모듈이 아닙니다 (딸린 함수를 쓰려면 가져오십시오)" o)
|
||||
else if not (Hashtbl.mem st.ctors o || List.mem o builtin_values) then
|
||||
error st pos (Printf.sprintf "%s은(는) 가져온 모듈도 지역 값도 아닙니다" o)
|
||||
| _ -> ());
|
||||
resolve_expr st obj
|
||||
| E_inst { callee; args; pos } ->
|
||||
@@ -261,7 +276,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 +365,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 +393,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
@@ -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))
|
||||
|
||||
@@ -20,6 +20,7 @@ type kind =
|
||||
| Kw_let
|
||||
| Kw_mut
|
||||
| Kw_own
|
||||
| Kw_self
|
||||
| Kw_affine
|
||||
| Kw_copyable
|
||||
| Kw_effects
|
||||
@@ -28,6 +29,8 @@ type kind =
|
||||
| Kw_else
|
||||
| Kw_match
|
||||
| Kw_scope
|
||||
| Kw_crash
|
||||
| Kw_test
|
||||
| Kw_true
|
||||
| Kw_false
|
||||
(* 구두점 *)
|
||||
@@ -79,6 +82,7 @@ let keyword = function
|
||||
| "let" -> Some Kw_let
|
||||
| "mut" -> Some Kw_mut
|
||||
| "own" -> Some Kw_own
|
||||
| "self" -> Some Kw_self
|
||||
| "affine" -> Some Kw_affine
|
||||
| "copyable" -> Some Kw_copyable
|
||||
| "effects" -> Some Kw_effects
|
||||
@@ -87,6 +91,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
|
||||
@@ -107,6 +113,7 @@ let show_kind = function
|
||||
| Kw_let -> "let"
|
||||
| Kw_mut -> "mut"
|
||||
| Kw_own -> "own"
|
||||
| Kw_self -> "self"
|
||||
| Kw_affine -> "affine"
|
||||
| Kw_copyable -> "copyable"
|
||||
| Kw_effects -> "effects"
|
||||
@@ -115,6 +122,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 +161,78 @@ 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_self
|
||||
| Kw_self -> Some Kw_affine
|
||||
| Kw_affine -> Some Kw_copyable
|
||||
| Kw_copyable -> Some Kw_effects
|
||||
| Kw_effects -> Some Kw_return
|
||||
| Kw_return -> Some Kw_if
|
||||
| Kw_if -> Some Kw_else
|
||||
| Kw_else -> Some Kw_match
|
||||
| Kw_match -> Some Kw_scope
|
||||
| Kw_scope -> Some Kw_crash
|
||||
| Kw_crash -> Some Kw_test
|
||||
| Kw_test -> Some Kw_true
|
||||
| Kw_true -> Some Kw_false
|
||||
| Kw_false -> Some LParen
|
||||
| LParen -> Some RParen
|
||||
| RParen -> Some LBrace
|
||||
| LBrace -> Some RBrace
|
||||
| RBrace -> Some LBracket
|
||||
| LBracket -> Some RBracket
|
||||
| RBracket -> Some Comma
|
||||
| Comma -> Some Colon
|
||||
| Colon -> Some Dot
|
||||
| Dot -> Some Arrow
|
||||
| Arrow -> Some FatArrow
|
||||
| FatArrow -> Some Question
|
||||
| Question -> Some Underscore
|
||||
| Underscore -> Some Eq
|
||||
| Eq -> Some EqEq
|
||||
| EqEq -> Some Bang
|
||||
| Bang -> Some BangEq
|
||||
| BangEq -> Some Lt
|
||||
| Lt -> Some Le
|
||||
| Le -> Some Gt
|
||||
| Gt -> Some Ge
|
||||
| Ge -> Some Plus
|
||||
| Plus -> Some Minus
|
||||
| Minus -> Some Star
|
||||
| Star -> Some Slash
|
||||
| Slash -> Some Percent
|
||||
| Percent -> Some AmpAmp
|
||||
| AmpAmp -> Some PipePipe
|
||||
| PipePipe -> Some Pipe
|
||||
| Pipe -> Some Newline
|
||||
| Newline -> Some Eof
|
||||
| Eof -> None
|
||||
|
||||
let all_kinds =
|
||||
let rec go k acc =
|
||||
match next_kind k with
|
||||
| None -> List.rev (k :: acc)
|
||||
| Some n -> go n (k :: acc)
|
||||
in
|
||||
go (Ident "") []
|
||||
|
||||
(* 줄 끝에서 문 구분자를 삽입할지 결정한다 (grammar.ebnf 어휘 절).
|
||||
값으로 끝날 수 있는 토큰 뒤에서만 삽입하므로, 연산자나 여는 괄호로
|
||||
끝나는 줄은 다음 줄로 이어진다. *)
|
||||
|
||||
+189
-27
@@ -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,10 +188,16 @@ 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
|
||||
| E_ident (n, _) -> (
|
||||
| E_ident (n, pos) -> (
|
||||
match lookup env n with
|
||||
| Some t -> t
|
||||
| None -> (
|
||||
@@ -185,9 +215,21 @@ let rec infer env (e : expr) : T.t =
|
||||
| None -> (
|
||||
match Hashtbl.find_opt env.ctors n with
|
||||
| Some enum -> nullary_ctor env enum n
|
||||
| None ->
|
||||
env.saw_unknown <- true;
|
||||
T.TUnknown)))))
|
||||
| None -> (
|
||||
(* 가져온 열거형의 생성자를 한정 없이 썼는가.
|
||||
모른다고 넘기면 그 자리가 통째로 불투명해지고,
|
||||
move 검사가 생성자인 줄 몰라 인자를 빌려 버린다 —
|
||||
즉 affine 값이 복제된다 (보안 정리 ii). *)
|
||||
match imported_ctor env n with
|
||||
| Some full ->
|
||||
err env pos
|
||||
(Printf.sprintf
|
||||
"%s은(는) 가져온 열거형의 생성자입니다 — %s로 적으십시오"
|
||||
n full);
|
||||
T.TUnknown
|
||||
| None ->
|
||||
env.saw_unknown <- true;
|
||||
T.TUnknown))))))
|
||||
| E_list (xs, pos) ->
|
||||
let elem = T.fresh () in
|
||||
List.iter
|
||||
@@ -197,7 +239,9 @@ let rec infer env (e : expr) : T.t =
|
||||
mismatch env pos elem t "리스트 원소의 타입이 서로 다릅니다")
|
||||
xs;
|
||||
T.TCon ("List", [ elem ])
|
||||
| E_struct { name; fields; pos } -> infer_struct env name fields pos
|
||||
| E_struct { modl; name; fields; pos } ->
|
||||
let key = match modl with Some a -> a ^ "." ^ name | None -> name in
|
||||
infer_struct env key fields pos
|
||||
| E_closure c -> infer_closure env c None
|
||||
| E_if { cond; then_; else_; pos } -> (
|
||||
let c = infer env cond in
|
||||
@@ -358,20 +402,31 @@ and infer_closure env (c : closure) (expected : T.t option) =
|
||||
match Option.map T.resolve expected with
|
||||
| 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,13 +475,30 @@ 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 ->
|
||||
let params, eff, ret = instantiate s in
|
||||
Some (T.TFn { affine = false; params; eff; ret })
|
||||
| None -> None)))
|
||||
| None -> (
|
||||
(* 가져온 열거형의 생성자를 한정 없이 불렀는가.
|
||||
모른다고 넘기면 move 검사가 생성자인 줄 몰라 인자를
|
||||
빌리고, affine 값이 복제된다 (보안 정리 ii). *)
|
||||
match imported_ctor env n with
|
||||
| Some full ->
|
||||
err env pos
|
||||
(Printf.sprintf
|
||||
"%s은(는) 가져온 열거형의 생성자입니다 — %s로 적으십시오" n full);
|
||||
None
|
||||
| None -> None))))
|
||||
| _ -> (
|
||||
match T.resolve (infer env callee) with
|
||||
| T.TFn _ as t -> Some t
|
||||
@@ -448,7 +520,7 @@ and infer_call env callee args pos =
|
||||
List.iter (fun a -> ignore (infer env a)) args)
|
||||
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 +553,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
|
||||
@@ -598,16 +670,63 @@ and check_pattern env (scrutinee : T.t) (p : pattern) =
|
||||
| L_bool _ -> T.TBool
|
||||
in
|
||||
if not (T.unify scrutinee t) then mismatch env pos scrutinee t "패턴의 리터럴"
|
||||
| P_bind (n, _) -> (
|
||||
| P_bind (n, pos) -> (
|
||||
match Hashtbl.find_opt env.ctors n with
|
||||
| Some enum ->
|
||||
check_ctor env scrutinee enum n [] Token.{ line = 0; col = 0 }
|
||||
| None -> bind env n scrutinee)
|
||||
| None -> (
|
||||
match qualified_variant env scrutinee n with
|
||||
| Some full -> variant_hint env pos n full
|
||||
| None -> bind env n scrutinee))
|
||||
| P_ctor { modl; name; args; pos } -> (
|
||||
let name = match modl with Some a -> a ^ "." ^ name | None -> name in
|
||||
match Hashtbl.find_opt env.ctors name with
|
||||
| Some enum -> check_ctor env scrutinee enum name args pos
|
||||
| None -> List.iter (check_pattern env T.TUnknown) args)
|
||||
let full = match modl with Some a -> a ^ "." ^ name | None -> name in
|
||||
match Hashtbl.find_opt env.ctors full with
|
||||
| Some enum -> check_ctor env scrutinee enum full args pos
|
||||
| None -> (
|
||||
match (modl, qualified_variant env scrutinee name) with
|
||||
| None, Some q -> variant_hint env pos name q
|
||||
| _ -> List.iter (check_pattern env T.TUnknown) args))
|
||||
|
||||
(* 한정되지 않은 이름이 match 대상 열거형의 variant를 가리키는가.
|
||||
가져온 열거형만 본다 (variant 이름에 점이 있다) — 이 모듈의 열거형이면
|
||||
env.ctors에서 이미 찾았을 것이기 때문이다.
|
||||
|
||||
이 진단이 없으면 인자 없는 variant를 한정 없이 적었을 때 그것이 바인딩이
|
||||
되고, 그 팔이 전부를 잡아 **다음 팔이 도달 불가**라는 엉뚱한 자리에
|
||||
오류가 난다. 틀린 줄을 가리키는 진단은 없느니만 못하다. *)
|
||||
and qualified_variant env scrutinee n =
|
||||
match T.resolve scrutinee with
|
||||
| T.TCon (enum, _) -> (
|
||||
match Hashtbl.find_opt env.enums enum with
|
||||
| Some (_, variants) ->
|
||||
List.find_opt
|
||||
(fun (v, _) ->
|
||||
match String.rindex_opt v '.' with
|
||||
| Some i -> String.sub v (i + 1) (String.length v - i - 1) = n
|
||||
| None -> false)
|
||||
variants
|
||||
|> Option.map fst
|
||||
| None -> None)
|
||||
| _ -> None
|
||||
|
||||
(* env.ctors에 "Alias.Name" 꼴로 들어 있는 생성자 중 이름이 n인 것.
|
||||
이 모듈의 생성자면 위에서 이미 찾았으므로 여기 오지 않는다. *)
|
||||
and imported_ctor env n =
|
||||
Hashtbl.fold
|
||||
(fun k _ acc ->
|
||||
match acc with
|
||||
| Some _ -> acc
|
||||
| None -> (
|
||||
match String.rindex_opt k '.' with
|
||||
| Some i when String.sub k (i + 1) (String.length k - i - 1) = n ->
|
||||
Some k
|
||||
| _ -> None))
|
||||
env.ctors None
|
||||
|
||||
and variant_hint env pos n full =
|
||||
err env pos
|
||||
(Printf.sprintf "%s은(는) 가져온 열거형의 variant입니다 — %s로 적으십시오 (한정 없는 이름은 바인딩입니다)"
|
||||
n full)
|
||||
|
||||
and check_ctor env scrutinee enum name args pos =
|
||||
match Hashtbl.find_opt env.enums enum with
|
||||
@@ -640,6 +759,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 +792,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 +905,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 +964,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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
// 파라미터 위치의 effect 절은 `effects <변수>` 또는 `effects {리터럴}`만 가능하다.
|
||||
// 합집합은 결과 위치에서만 쓸 수 있다 — 문법이 그렇게 생겼다.
|
||||
|
||||
import "cool.dev/std/string" as String
|
||||
|
||||
pub fn map[a, b, e: effects](
|
||||
xs: List[a],
|
||||
f: fn(a) effects e -> b,
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
//
|
||||
// variant 추가가 downstream exhaustive match를 깨뜨리는 경로.
|
||||
|
||||
import "cool.dev/std/string" as String
|
||||
import "cool.dev/std/int" as Int
|
||||
|
||||
// 거절 코드. 예전에는 어디에도 없는 이름이었고 Code.show가 조용히
|
||||
// 통과했다 — 이름 해소가 점 왼쪽을 검사하지 않아서다.
|
||||
pub copyable struct Code {
|
||||
value: Int,
|
||||
}
|
||||
|
||||
pub fn show_code(c: Code) -> String {
|
||||
Int.show(c.value)
|
||||
}
|
||||
|
||||
pub enum PayError {
|
||||
Declined(Code),
|
||||
Network(Retryable),
|
||||
@@ -24,7 +37,7 @@ pub fn is_retryable(e: PayError) -> Bool {
|
||||
|
||||
pub fn describe(e: PayError) -> String {
|
||||
match e {
|
||||
Declined(code) => String.concat("declined: ", Code.show(code)),
|
||||
Declined(code) => String.concat("declined: ", show_code(code)),
|
||||
Network(_) => "network",
|
||||
Fraud => "fraud",
|
||||
}
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
//
|
||||
// affinity는 타입 속성, own/무표기는 파라미터 위치 속성. 둘은 직교한다.
|
||||
|
||||
import "cool.dev/std/string" as String
|
||||
|
||||
// 예전에는 File이 어디에도 없는 이름이었고 File.close(f)가 조용히
|
||||
// 통과했다. 이제 선언한다 — 그리고 own self가 있으므로 닫는 일은
|
||||
// 핸들 자신의 메서드다.
|
||||
pub capability File {
|
||||
fn close(own self) effects {File.close}
|
||||
}
|
||||
|
||||
// copyable 환경만 capture → fn. 복사해서 여러 번 써도 된다.
|
||||
pub fn make_formatter(prefix: String) -> fn(String) -> String {
|
||||
fn(s) { String.concat(prefix, s) }
|
||||
@@ -9,7 +18,7 @@ pub fn make_formatter(prefix: String) -> fn(String) -> String {
|
||||
|
||||
// affine 값을 소유해 가므로 own. 결과 클로저는 자동으로 affine fn이 된다.
|
||||
pub fn deferred_close(own f: File) -> affine fn() effects {File.close} {
|
||||
fn() { File.close(f) }
|
||||
fn() { f.close() }
|
||||
}
|
||||
|
||||
// affine fn을 호출 후 소비하려면 소유해야 한다.
|
||||
|
||||
@@ -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")))
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# 예제 설정
|
||||
|
||||
name = coollang
|
||||
threads = 4
|
||||
port = 8080
|
||||
verbose = true
|
||||
greeting = hello world
|
||||
|
||||
# 아래 두 줄은 일부러 틀렸다
|
||||
= 42
|
||||
broken = a = b
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
+44
-2
@@ -2,14 +2,43 @@
|
||||
//
|
||||
// 본문이 없다. 런타임이 구현하고, 이 파일은 그 계약을 말한다.
|
||||
// 그래서 이 파일은 구현이 아니라 시험대다 — 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]
|
||||
|
||||
// 첫 원소를 뺀 나머지. 비어 있으면 빈 리스트다 — 없는 것을 빼는 일은
|
||||
// 오류가 아니라 아무 일도 아니다. first와 짝이고, 둘은 서로 어긋날 수 없다.
|
||||
pub fn rest[a](xs: List[a]) -> List[a]
|
||||
|
||||
// 번호를 붙인 값. fold에 인덱스를 넣는 대신 이것 하나를 둔다 —
|
||||
// fold_indexed를 만들면 map_indexed, each_indexed가 따라오고 그것이
|
||||
// "한 개념 한 방식"을 깨는 방향이다. enumerate는 기존 각 함수와 조합된다.
|
||||
pub copyable struct Indexed[a] {
|
||||
i: Int,
|
||||
value: a,
|
||||
}
|
||||
|
||||
pub fn enumerate[a](xs: List[a]) -> List[Indexed[a]]
|
||||
|
||||
// 뒤에 하나 붙인 새 리스트. 제자리 수정이 아니다.
|
||||
pub fn push[a](xs: List[a], x: a) -> List[a]
|
||||
|
||||
pub fn concat[a](xs: List[a], ys: List[a]) -> List[a]
|
||||
|
||||
pub fn reverse[a](xs: List[a]) -> List[a]
|
||||
|
||||
pub fn each[a, e: effects](
|
||||
xs: List[a],
|
||||
f: fn(a) effects e,
|
||||
@@ -19,3 +48,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
|
||||
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// 표준 라이브러리: 테스트.
|
||||
//
|
||||
// assert는 특별한 것이 아니라 crash 위의 설탕이다. 그래서 본문이 있다 —
|
||||
// std에서 실제 coollang으로 쓰인 첫 함수다.
|
||||
//
|
||||
// 실패 메시지에 값이 안 나오는 것은 의도다. `assert(a == b)`에서 a와 b를
|
||||
// 보여주려면 표현식 텍스트를 잡아야 하고 그건 매크로다. 테스트 이름과
|
||||
// 위치가 어느 것이 깨졌는지 말해준다.
|
||||
|
||||
pub fn assert(c: Bool) {
|
||||
if !c {
|
||||
crash("assert 실패")
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
(test
|
||||
(name test_coollang)
|
||||
(libraries coollang)
|
||||
(libraries coollang unix)
|
||||
(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)))
|
||||
|
||||
+882
-13
@@ -180,11 +180,6 @@ let () =
|
||||
check "버전 비어있지 않음" (String.length Version.string > 0);
|
||||
match Driver.check [] with Error [ _ ] -> () | _ -> check "빈 입력에는 오류" false
|
||||
|
||||
let () =
|
||||
if !failures = 0 then print_endline "ok"
|
||||
else (
|
||||
Printf.printf "%d개 실패\n" !failures;
|
||||
exit 1)
|
||||
|
||||
(* --- ASI 함정: effects 절이 줄 끝에 오면 } 뒤에 NEWLINE이 삽입된다.
|
||||
렉서는 이대로 두고 문법이 { NEWLINE }으로 흡수한다 (grammar.ebnf). --- *)
|
||||
@@ -327,8 +322,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 +388,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 +1027,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 +1110,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 +1119,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 +1252,852 @@ 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/careful.cool",
|
||||
[ "../dogfoods/std-draft/fs.cool" ] );
|
||||
("../dogfoods/std-draft/db.cool", []);
|
||||
("../dogfoods/db-pool/careful.cool", [ "../dogfoods/std-draft/db.cool" ]);
|
||||
("../dogfoods/lru-cache/values.cool", []);
|
||||
( "../dogfoods/lru-cache/resources.cool",
|
||||
[ "../dogfoods/std-draft/db.cool" ] );
|
||||
("../dogfoods/std-draft/os.cool", []);
|
||||
("../dogfoods/mini-shell/parse.cool", []);
|
||||
( "../dogfoods/mini-shell/shell.cool",
|
||||
[ "../dogfoods/std-draft/os.cool"; "../dogfoods/mini-shell/parse.cool" ]
|
||||
);
|
||||
("../dogfoods/std-draft/sup.cool", []);
|
||||
( "../dogfoods/supervisor/workers.cool",
|
||||
[ "../dogfoods/std-draft/sup.cool"; "../dogfoods/std-draft/db.cool" ] );
|
||||
]
|
||||
in
|
||||
List.iter
|
||||
(fun (path, deps) ->
|
||||
let st = Session.create ~root:(Filename.dirname path) ~std:"../std" () in
|
||||
Session.load st path;
|
||||
let errs = Session.errors st in
|
||||
List.iter
|
||||
(fun (e : Session.error) ->
|
||||
Printf.printf " (%s)\n" (Session.string_of_error e))
|
||||
errs;
|
||||
check (Filename.basename path ^ ": check 통과") (errs = []);
|
||||
(* 의존 모듈이 실제로 로드됐는가 — 이게 없으면 위 줄이 공허하다.
|
||||
경로 표기가 여러 가지라(../a/../b) 파일 이름으로 맞춘다 *)
|
||||
let loaded =
|
||||
Hashtbl.fold
|
||||
(fun k _ acc -> Filename.basename k :: acc)
|
||||
st.Session.modules []
|
||||
in
|
||||
List.iter
|
||||
(fun d ->
|
||||
check
|
||||
(Filename.basename path ^ ": " ^ Filename.basename d
|
||||
^ "이(가) 실제로 해소된다")
|
||||
(List.mem (Filename.basename d) loaded))
|
||||
deps)
|
||||
cases
|
||||
|
||||
(* Mini Shell의 파싱은 순수하므로 실제로 돌릴 수 있다. 개밥 먹기 중
|
||||
처음으로 "쓰기만 한 것"이 아니라 "돌아가는 것"이 나왔다. *)
|
||||
let () =
|
||||
let st = Session.create ~root:"../dogfoods/mini-shell" ~std:"../std" () in
|
||||
match Session.test st "../dogfoods/mini-shell/parse.cool" with
|
||||
| Error es ->
|
||||
List.iter
|
||||
(fun (e : Session.error) ->
|
||||
Printf.printf " (%s)\n" (Session.string_of_error e))
|
||||
es;
|
||||
check "mini-shell 파서 테스트가 돈다" false
|
||||
| Ok results ->
|
||||
check "mini-shell 파서 테스트가 다섯 개다" (List.length results = 5);
|
||||
check "mini-shell 파서 테스트가 전부 통과한다"
|
||||
(List.for_all (fun (r : Interp.test_result) -> r.t_failure = None) results)
|
||||
|
||||
(* D3: 자원을 들고 ?를 쓰면 정리를 건너뛴다.
|
||||
v0 정책은 여전히 "오용 금지, 누수 허용"이다 — 타입 층에서 막지 않는다.
|
||||
다만 이제 lint가 그 자리를 가리킨다. 언어가 강제로 막을지는 `with`가
|
||||
결정할 문제이고, 그 결정에는 "실제 코드에서 얼마나 자주 새는가"라는
|
||||
측정이 필요하다. lint가 그 측정이다.
|
||||
|
||||
naive와 careful 쌍이 이 개밥 먹기의 산출물이므로, lint가 정확히 naive
|
||||
쪽에서만 울리는지가 그 쌍이 뜻을 갖는 조건이다. *)
|
||||
let () =
|
||||
let leaks root path =
|
||||
let st = Session.create ~root ~std:"../std" () in
|
||||
Session.load st path;
|
||||
List.filter
|
||||
(fun (e : Session.error) ->
|
||||
let m = e.message in
|
||||
let frag = "정리되지 않습니다" in
|
||||
let n = String.length frag in
|
||||
let rec go i =
|
||||
i + n <= String.length m && (String.sub m i n = frag || go (i + 1))
|
||||
in
|
||||
go 0)
|
||||
(Session.errors st)
|
||||
in
|
||||
let a = leaks "../dogfoods/atomic-update" "../dogfoods/atomic-update/naive.cool" in
|
||||
let b =
|
||||
leaks "../dogfoods/atomic-update" "../dogfoods/atomic-update/careful.cool"
|
||||
in
|
||||
let c = leaks "../dogfoods/db-pool" "../dogfoods/db-pool/naive.cool" in
|
||||
let d = leaks "../dogfoods/db-pool" "../dogfoods/db-pool/careful.cool" in
|
||||
check "naive는 ?마다 새는 것을 가리킨다" (List.length a = 2);
|
||||
check "careful은 조용하다 (파일)" (b = []);
|
||||
check "naive는 두 층 모두 가리킨다 (풀)" (List.length c = 2);
|
||||
check "careful은 조용하다 (풀)" (d = []);
|
||||
(* 누수는 여전히 타입 층의 오류가 아니다 — lint 말고 다른 것이 없어야 한다*)
|
||||
let st = Session.create ~root:"../dogfoods/atomic-update" ~std:"../std" () in
|
||||
Session.load st "../dogfoods/atomic-update/naive.cool";
|
||||
check "누수 말고는 아무 오류도 없다 (오용 금지, 누수 허용)"
|
||||
(List.length (Session.errors st) = 2)
|
||||
|
||||
(* D10 회귀: Option/Result 패턴이 타입을 나른다.
|
||||
전에는 내장 생성자가 typecheck의 ctors에 없어 None이 변수 바인딩이 되고
|
||||
Ok(v)의 v가 TUnknown이었다 — 즉 핵심 오류 처리 수단의 패턴 매칭이 타입을
|
||||
전혀 검사하지 않았다. *)
|
||||
let () =
|
||||
check "Ok(v)의 v가 타입을 갖는다"
|
||||
(type_has
|
||||
"fn f(r: Result[Int, String]) -> String {\n\
|
||||
\ match r {\n\
|
||||
\ Ok(v) => v,\n\
|
||||
\ Err(e) => e,\n\
|
||||
\ }\n\
|
||||
}"
|
||||
"String이(가) 필요한데 Int");
|
||||
check "None은 변수 바인딩이 아니다"
|
||||
(type_has
|
||||
"fn f(o: Option[Int]) -> Int {\n\
|
||||
\ match o {\n\
|
||||
\ None => \"x\",\n\
|
||||
\ Some(v) => v,\n\
|
||||
\ }\n\
|
||||
}"
|
||||
"match 팔의 타입이 서로 다릅니다")
|
||||
|
||||
(* 보안 정리 (ii): safe code에서 capability는 복제되지 않는다.
|
||||
struct 필드를 통한 구멍이 있었다 — 필드 접근이 빌린 값을 돌려주는데
|
||||
Move 자리에서 그것을 검사하는 곳이 없었다. 빌린 wrapper에서도 됐다. *)
|
||||
let () =
|
||||
let src =
|
||||
"capability Pay {\n\
|
||||
\ fn charge(n: Int) effects {Pay.charge}\n\
|
||||
}\n\n\
|
||||
struct Wrapper {\n\
|
||||
\ pay: Pay,\n\
|
||||
\ label: String,\n\
|
||||
}\n\n\
|
||||
fn consume(own p: Pay) effects {Pay.charge}\n\n"
|
||||
in
|
||||
check "capability를 필드로 복제할 수 없다"
|
||||
(move_has
|
||||
(src
|
||||
^ "fn duplicate(w: Wrapper) effects {Pay.charge} {\n\
|
||||
\ consume(w.pay)\n\
|
||||
\ consume(w.pay)\n\
|
||||
}")
|
||||
"부분 move가 없습니다");
|
||||
check "소유한 struct에서도 필드를 꺼낼 수 없다"
|
||||
(move_has
|
||||
(src
|
||||
^ "fn take(own w: Wrapper) effects {Pay.charge} {\n consume(w.pay)\n}")
|
||||
"부분 move가 없습니다");
|
||||
check "copyable 필드는 막지 않는다"
|
||||
(move_errs (src ^ "fn label_of(w: Wrapper) -> String {\n w.label\n}") = [])
|
||||
|
||||
(* ================================================================== *)
|
||||
(* D4 — 수신자를 소비하는 capability 메서드 (own self) *)
|
||||
(* *)
|
||||
(* 이것이 없으면 자원이 자기 정리를 스스로 선언할 수 없고, 정리에 늘 *)
|
||||
(* 바깥 권한이 따라붙는다. 그러면 with가 자원만 보고 무엇을 불러야 *)
|
||||
(* 할지 알 수 없다 (dogfoods/FINDINGS D9). *)
|
||||
(* ================================================================== *)
|
||||
|
||||
let selfsrc =
|
||||
"capability Lease {\n\
|
||||
\ fn query(s: String) effects {Lease.query} -> Int\n\
|
||||
\ fn release(own self) effects {Lease.release}\n\
|
||||
}\n\n"
|
||||
|
||||
let () =
|
||||
check "own self는 수신자를 소비한다 — 한 번은 된다"
|
||||
(move_ok
|
||||
(selfsrc
|
||||
^ "fn once(own l: Lease) effects {Lease.release} {\n l.release()\n}"));
|
||||
check "두 번 소비하면 잡힌다"
|
||||
(move_has
|
||||
(selfsrc
|
||||
^ "fn twice(own l: Lease) effects {Lease.release} {\n\
|
||||
\ l.release()\n\
|
||||
\ l.release()\n\
|
||||
}")
|
||||
"이미 move되었습니다");
|
||||
check "빌린 수신자는 소비할 수 없다"
|
||||
(move_has
|
||||
(selfsrc
|
||||
^ "fn borrowed(l: Lease) effects {Lease.release} {\n l.release()\n}")
|
||||
"빌린 값이라 release()로 소비할 수 없습니다");
|
||||
check "빌리는 메서드는 몇 번이든 부를 수 있다"
|
||||
(move_ok
|
||||
(selfsrc
|
||||
^ "fn twice_query(l: Lease) effects {Lease.query} -> Int {\n\
|
||||
\ let a = l.query(\"x\")\n\
|
||||
\ l.query(\"y\")\n\
|
||||
}"));
|
||||
(* 중첩 수신자. 예전에는 시그니처를 못 찾아 조용히 빌림이 됐다 —
|
||||
그러면 필드에 담긴 자원을 두 번 소비할 수 있었다. *)
|
||||
check "struct 필드에 담긴 자원도 소비할 수 없다"
|
||||
(move_has
|
||||
(selfsrc
|
||||
^ "struct Held { lease: Lease }\n\
|
||||
fn f(h: Held) effects {Lease.release} {\n h.lease.release()\n}")
|
||||
"부분 move가 없습니다");
|
||||
(* 문법: own self는 capability 메서드의 첫 자리에만 존재한다 *)
|
||||
check "일반 함수에는 self 자리가 없다"
|
||||
(parse_err "fn f(own self) {}" = Some "self는 capability 메서드에서만 쓸 수 있습니다");
|
||||
check "own 없는 self는 적을 말이 없다"
|
||||
(parse_err "capability C {\n fn m(self)\n}"
|
||||
= Some "self는 own self로만 적습니다 (수신자를 빌리는 것이 기본이라 적을 말이 없습니다)");
|
||||
check "self는 첫 자리에만 온다"
|
||||
(parse_err "capability C {\n fn m(x: Int, own self)\n}"
|
||||
= Some "self는 capability 메서드의 첫 파라미터로만 올 수 있습니다")
|
||||
|
||||
(* 수신자를 소비하는지는 표면이다 — 바뀌면 호출하는 쪽의 move 검사 결과가
|
||||
바뀌므로 interface hash에 들어가야 한다 *)
|
||||
let () =
|
||||
let h src = (Iface.of_module (parse_ok src)).Iface.hash in
|
||||
check "own self가 interface hash를 바꾼다"
|
||||
(h "pub capability C {\n fn m(own self)\n}"
|
||||
<> h "pub capability C {\n fn m()\n}")
|
||||
|
||||
(* 보안 정리 (ii), 두 번째 구멍: 열거형 생성자가 인자를 소비하지 않았다.
|
||||
리스트 리터럴과 struct 리터럴은 처음부터 Move 자리였는데 생성자만
|
||||
일반 호출로 취급돼 빠져 있었다. Some(f)를 두 번 쓰면 복제됐다.
|
||||
LRU 개밥 먹기가 affine 값을 열거형으로 실어나르는 방식을 쓰므로
|
||||
이 자리가 뚫려 있으면 그 우회 자체가 무의미하다. *)
|
||||
let () =
|
||||
let src =
|
||||
"capability Fd {\n\
|
||||
\ fn close(own self) effects {Fd.close}\n\
|
||||
}\n\
|
||||
enum Maybe { Just(Fd), Nothing }\n\n"
|
||||
in
|
||||
check "사용자 열거형 생성자가 인자를 소비한다"
|
||||
(move_has
|
||||
(src
|
||||
^ "fn dup(own f: Fd) -> Maybe {\n let a = Just(f)\n Just(f)\n}")
|
||||
"이미 move되었습니다");
|
||||
check "내장 Option 생성자도 소비한다"
|
||||
(move_has
|
||||
(src
|
||||
^ "fn dup(own f: Fd) -> Option[Fd] {\n let a = Some(f)\n Some(f)\n}")
|
||||
"이미 move되었습니다");
|
||||
check "내장 Result 생성자도 소비한다"
|
||||
(move_has
|
||||
(src
|
||||
^ "fn dup(own f: Fd) -> Result[Fd, Int] {\n let a = Ok(f)\n Ok(f)\n}")
|
||||
"이미 move되었습니다");
|
||||
check "한 번만 넣는 것은 된다"
|
||||
(move_ok (src ^ "fn wrap(own f: Fd) -> Maybe {\n Just(f)\n}"));
|
||||
check "빌린 값은 열거형에 넣을 수 없다"
|
||||
(move_has
|
||||
(src ^ "fn wrap(f: Fd) -> Maybe {\n Just(f)\n}")
|
||||
"빌린 값이라 열거형에 넣을 수 없습니다");
|
||||
check "copyable 값은 자유롭게 넣는다"
|
||||
(move_ok (src ^ "fn n(x: Int) -> Option[Int] {\n let a = Some(x)\n Some(x)\n}"))
|
||||
|
||||
(* 점 왼쪽이 아무것도 아니면 그 이름은 없는 것이다.
|
||||
예전에는 바깥 참조로 넘어가 오른쪽이 통째로 불투명해졌다 — D1과 같은
|
||||
병이다. Nonsense.whatever(1)이 조용히 통과했다. *)
|
||||
let () =
|
||||
let msgs src =
|
||||
List.map (fun (e : Resolve.error) -> e.msg) (snd (Resolve.resolve (parse_ok src)))
|
||||
in
|
||||
let contains m frag =
|
||||
let n = String.length frag in
|
||||
let rec go i =
|
||||
i + n <= String.length m && (String.sub m i n = frag || go (i + 1))
|
||||
in
|
||||
go 0
|
||||
in
|
||||
let has src frag = List.exists (fun m -> contains m frag) (msgs src) in
|
||||
check "가져오지 않은 이름에 점을 찍을 수 없다"
|
||||
(has "fn f() -> Bool {\n Nonsense.whatever(1)\n}" "가져온 모듈도 지역 값도 아닙니다");
|
||||
check "내장 타입은 모듈이 아니다"
|
||||
(has "fn f(s: String) -> Int {\n String.len(s)\n}" "내장 타입이지 모듈이 아닙니다");
|
||||
check "가져온 별칭은 통과한다"
|
||||
(not
|
||||
(has
|
||||
"import \"cool.dev/std/string\" as String\nfn f(s: String) -> Int {\n String.len(s)\n}"
|
||||
"모듈"));
|
||||
(* capability 값 없이 메서드를 부르는 것은 타입 검사의 일이다.
|
||||
여기서 막으면 그쪽의 더 정확한 진단에 닿지 못한다 *)
|
||||
check "이 모듈의 선언은 통과시킨다 (진단은 타입 검사가 한다)"
|
||||
(not
|
||||
(has
|
||||
"capability Db {\n fn read(k: Int) -> Int\n}\nfn f() -> Int {\n Db.read(1)\n}"
|
||||
"가져온 모듈도"))
|
||||
|
||||
(* 보안 정리 (ii), 세 번째 구멍: 소유한 열거형을 두 번 분해할 수 있었다.
|
||||
match가 scrutinee를 늘 빌려서, 조각을 owned로 꺼내 놓고도 원본이
|
||||
소비되지 않았다. 파이프 하나에서 같은 fd를 두 번 꺼낼 수 있다는 뜻이다. *)
|
||||
let () =
|
||||
let src =
|
||||
"capability Fd {\n\
|
||||
\ fn close(own self) effects {Fd.close}\n\
|
||||
}\n\
|
||||
enum Pipe { Ends(Fd, Fd) }\n\
|
||||
fn take(own f: Fd) effects {Fd.close} {\n\
|
||||
\ f.close()\n\
|
||||
}\n\n"
|
||||
in
|
||||
let twice =
|
||||
"fn dup(own p: Pipe) effects {Fd.close} {\n\
|
||||
\ match p {\n\
|
||||
\ Ends(a, b) => {\n\
|
||||
\ take(a)\n\
|
||||
\ take(b)\n\
|
||||
\ },\n\
|
||||
\ }\n\
|
||||
\ match p {\n\
|
||||
\ Ends(a, b) => {\n\
|
||||
\ take(a)\n\
|
||||
\ take(b)\n\
|
||||
\ },\n\
|
||||
\ }\n\
|
||||
}"
|
||||
in
|
||||
check "소유한 열거형을 두 번 분해할 수 없다"
|
||||
(move_has (src ^ twice) "이미 move되었습니다");
|
||||
check "한 번 분해해 꺼내는 것은 된다"
|
||||
(move_ok
|
||||
(src
|
||||
^ "fn once(own p: Pipe) effects {Fd.close} {\n\
|
||||
\ match p {\n\
|
||||
\ Ends(a, b) => {\n\
|
||||
\ take(a)\n\
|
||||
\ take(b)\n\
|
||||
\ },\n\
|
||||
\ }\n\
|
||||
}"));
|
||||
check "빌린 열거형에서는 꺼낼 수 없다"
|
||||
(move_has
|
||||
(src
|
||||
^ "fn borrowed(p: Pipe) effects {Fd.close} {\n\
|
||||
\ match p {\n\
|
||||
\ Ends(a, b) => take(a),\n\
|
||||
\ }\n\
|
||||
}")
|
||||
"빌린 값이라");
|
||||
(* 이름을 묶지 않는 패턴은 아무것도 꺼내지 않으므로 소비하지 않는다 *)
|
||||
check "이름을 묶지 않는 분해는 소비하지 않는다"
|
||||
(move_ok
|
||||
(src
|
||||
^ "fn peek(own p: Pipe) effects {Fd.close} {\n\
|
||||
\ match p {\n\
|
||||
\ Ends(_, _) => 1,\n\
|
||||
\ }\n\
|
||||
\ match p {\n\
|
||||
\ Ends(a, b) => {\n\
|
||||
\ take(a)\n\
|
||||
\ take(b)\n\
|
||||
\ },\n\
|
||||
\ }\n\
|
||||
}"))
|
||||
|
||||
(* 가져온 모듈의 struct를 만들 수 없었다. 타입은 쓸 수 있는데 값은 못
|
||||
만드는 상태였고, 개밥 넷이 전부 같은 모듈 안에서만 struct를 만들어
|
||||
한 번도 안 걸렸다.
|
||||
|
||||
실제로 해소까지 되는지는 supervisor/workers.cool이 Sup.Policy를 만들면서
|
||||
확인한다 (위 dogfoods 목록). 여기서는 문법만 고정한다. *)
|
||||
let () =
|
||||
let parses src = Result.is_ok (Parser.parse_result (Result.get_ok (Lexer.lex_result src))) in
|
||||
check "Alias.Name { ... } 를 파서가 받는다"
|
||||
(parses "fn f() -> P.P {\n P.P { n: 1 }\n}");
|
||||
(* no_struct 문맥에서는 "{" 가 언제나 블록이다. 여기서 점을 먹으면
|
||||
if a.b { } 의 블록이 struct 리터럴로 읽힌다 *)
|
||||
check "if의 머리에서는 Alias.Name 뒤의 { 가 블록이다"
|
||||
(parses "fn f(a: Int) -> Int {\n if A.b { 1 } else { 2 }\n}");
|
||||
check "match의 머리에서도 그렇다"
|
||||
(parses "fn f(a: Int) -> Int {\n match A.b {\n _ => 1,\n }\n}")
|
||||
|
||||
(* 가져온 열거형의 이름을 한정 없이 쓰면 조용히 통과했다.
|
||||
- 패턴에서는 바인딩이 되어 그 팔이 전부를 잡고, 오류가 다음 팔에 났다
|
||||
- 식에서는 모르는 이름이 되어 move 검사가 생성자인 줄 몰랐다.
|
||||
그래서 affine 값을 두 번 감쌀 수 있었다 (보안 정리 ii)
|
||||
Mini Shell에 다섯 군데 있었고 파이프 처리 전체가 검사되지 않고 있었다. *)
|
||||
let () =
|
||||
let dep =
|
||||
"pub capability Fd {\n\
|
||||
\ fn close(own self) effects {Fd.close}\n\
|
||||
}\n\
|
||||
pub enum Box { Wrap(Fd), Empty }\n"
|
||||
in
|
||||
let msgs src =
|
||||
let dir = Filename.temp_file "cool" "" in
|
||||
Sys.remove dir;
|
||||
Unix.mkdir dir 0o755;
|
||||
let w n c =
|
||||
let oc = open_out (Filename.concat dir n) in
|
||||
output_string oc c;
|
||||
close_out oc
|
||||
in
|
||||
w "k.cool" dep;
|
||||
w "j.cool" src;
|
||||
let st = Session.create ~root:dir ~std:"../std" () in
|
||||
Session.load st (Filename.concat dir "j.cool");
|
||||
List.map (fun (e : Session.error) -> e.message) (Session.errors st)
|
||||
in
|
||||
let has src frag =
|
||||
List.exists
|
||||
(fun m ->
|
||||
let n = String.length frag in
|
||||
let rec go i =
|
||||
i + n <= String.length m && (String.sub m i n = frag || go (i + 1))
|
||||
in
|
||||
go 0)
|
||||
(msgs src)
|
||||
in
|
||||
check "한정 없는 가져온 생성자를 식에서 거절한다"
|
||||
(has
|
||||
"import \"./k\" as K\nfn f(own x: K.Fd) -> K.Box {\n Wrap(x)\n}"
|
||||
"K.Wrap로 적으십시오");
|
||||
check "한정 없는 가져온 variant를 패턴에서 거절한다"
|
||||
(has
|
||||
"import \"./k\" as K\nfn f(b: K.Box) -> Int {\n\
|
||||
\ match b {\n Empty => 0,\n K.Wrap(_) => 1,\n }\n}"
|
||||
"K.Empty로 적으십시오");
|
||||
check "한정하면 통과하고 이중 소비가 잡힌다"
|
||||
(has
|
||||
"import \"./k\" as K\nfn f(own x: K.Fd) -> K.Box {\n\
|
||||
\ let a = K.Wrap(x)\n K.Wrap(x)\n}"
|
||||
"이미 move되었습니다");
|
||||
check "한정한 한 번은 통과한다"
|
||||
(msgs "import \"./k\" as K\nfn f(own x: K.Fd) -> K.Box {\n K.Wrap(x)\n}" = [])
|
||||
|
||||
(* ================================================================== *)
|
||||
(* 요약. 반드시 파일의 맨 끝이어야 한다. *)
|
||||
(* *)
|
||||
(* 예전에는 이 블록이 파일 184줄에 있었다. 최상위 let ()은 파일 순서대로 *)
|
||||
(* 돌므로 그 뒤에 쓴 1500줄의 검사는 FAIL을 찍고도 종료 코드에 잡히지 *)
|
||||
(* 않았다 — 즉 실패한 테스트를 커밋할 수 있었다. 빠른 검증 루프가 *)
|
||||
(* 목적인 저장소에서 검증이 조용히 통과하는 것이 가장 나쁜 결함이다. *)
|
||||
(* ================================================================== *)
|
||||
|
||||
let () =
|
||||
if !failures = 0 then print_endline "ok"
|
||||
else (
|
||||
Printf.printf "%d개 실패\n" !failures;
|
||||
exit 1)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
(executable
|
||||
(name ebnf_tool)
|
||||
(libraries coollang))
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user