Compare commits
18
Commits
5ac899df68
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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의 질문은 "되돌리기 비싼 결정이 옳은가"였다. 답은 **그렇다**이고,
|
||||
남은 불편은 전부 되돌리기 싼 것들이다.
|
||||
+177
-82
@@ -7,10 +7,24 @@
|
||||
* { } 반복 (0 이상)
|
||||
* ( ) 묶음
|
||||
* " " 단말
|
||||
* < > 매개변수 (아래 참고)
|
||||
* (* *) 주석
|
||||
*
|
||||
* 설계 제약: LL(1). backtracking 없음, 렉서 피드백 없음.
|
||||
* 어떤 프로덕션도 무한 선읽기를 요구해서는 안 된다 (철학 2).
|
||||
* 이 제약은 주장이 아니라 검사된다 — tools/ebnf_tool.exe가 이 파일을 읽어
|
||||
* FIRST/FOLLOW를 계산하고 충돌을 보고하며, 충돌이 있으면 테스트가 깨진다.
|
||||
*
|
||||
* 매개변수 프로덕션:
|
||||
* name<p> = ... p ... 로 정의하고 name<arg> 로 참조한다.
|
||||
* 인자마다 하나의 평범한 프로덕션으로 펼쳐진다. 인자에 따라 몸통이 달라야
|
||||
* 하면 머리에 인자를 박아 특수화한다 (name<yes> = ... ; name<no> = ... ;).
|
||||
* 목록과 expr_ns를 복제 없이 적기 위한 것이다.
|
||||
*
|
||||
* 흡수 규칙:
|
||||
* [ NEWLINE ]은 최대한 먹는다(greedy). 줄바꿈은 값을 갖지 않으므로 바깥
|
||||
* 프로덕션이 먹든 안쪽이 먹든 파스 트리가 같다. 이 규칙이 없으면 흡수
|
||||
* 지점마다 형식적 중의성이 생기지만, 어느 쪽으로 읽어도 결과가 같다.
|
||||
*)
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
@@ -20,43 +34,84 @@
|
||||
(* 주석은 // 부터 줄 끝까지. 블록 주석 없음 (중첩 규칙이라는 변종을 만들지 않는다) *)
|
||||
|
||||
(* 문 구분자 NEWLINE은 렉서가 삽입한다:
|
||||
* 줄의 마지막 토큰이 ident, 리터럴, ")", "]", "}", "?", "return" 중 하나이면
|
||||
* 그 줄 끝에 NEWLINE 토큰을 넣는다. 그 외에는 넣지 않는다.
|
||||
* 따라서 연산자나 여는 괄호로 끝나는 줄은 다음 줄로 이어진다.
|
||||
* 줄의 마지막 토큰이 값으로 끝날 수 있는 토큰이면 그 줄 끝에 NEWLINE을 넣는다.
|
||||
* 그 목록은 아래 생성 블록에 있다 — 손으로 적지 않고 코드에서 뽑는다.
|
||||
* 그 외에는 넣지 않는다. 따라서 연산자나 여는 괄호로 끝나는 줄은 이어진다.
|
||||
* 빈 줄과 주석만 있는 줄은 NEWLINE을 만들지 않는다.
|
||||
*
|
||||
* NEWLINE은 결코 연달아 나오지 않는다. 삽입 판정이 "마지막으로 낸 토큰"을
|
||||
* 보는데 NEWLINE 자신은 값으로 끝날 수 있는 토큰이 아니기 때문이다.
|
||||
* 그래서 이 문법의 모든 흡수 지점은 { NEWLINE }이 아니라 [ NEWLINE ]이다.
|
||||
*
|
||||
(* 여기부터 lib/token.ml에서 생성됩니다 — 손으로 고치지 마십시오 *)
|
||||
* 줄을 끝낼 수 있는 토큰. 줄의 마지막 토큰이 이 중 하나이면 그 줄 끝에
|
||||
* NEWLINE이 삽입된다. Token.can_end_statement가 원본이다.
|
||||
*
|
||||
* ident | int_lit | string_lit | "return" | "true" | "false" | ")" |
|
||||
* "}" | "]" | "?"
|
||||
*
|
||||
* 키워드. 이름으로 쓸 수 없다. Token.keyword가 원본이다.
|
||||
*
|
||||
* "pub" | "fn" | "struct" | "enum" | "capability" | "const" |
|
||||
* "import" | "as" | "reexport" | "let" | "mut" | "own" | "affine" |
|
||||
* "copyable" | "effects" | "return" | "if" | "else" | "match" |
|
||||
* "scope" | "crash" | "test" | "true" | "false"
|
||||
(* 생성 끝 *)
|
||||
*
|
||||
* 다중 줄 목록(파라미터, 인자, 필드, variant, 리스트 리터럴)은 후행 콤마가
|
||||
* 필수다. 콤마로 끝난 줄은 NEWLINE을 만들지 않으므로 목록이 자연히 이어진다.
|
||||
* 공식 formatter가 이를 강제한다.
|
||||
*
|
||||
* 중괄호 목록(struct 정의, enum 정의, struct 리터럴, effect 집합, match 팔)
|
||||
* 안에서는 항목 구분자 주변의 NEWLINE이 무시된다. 문이 놓이는 자리가 아니므로
|
||||
* 구분자로 쓰이지 않는다. 아래 프로덕션에 { NEWLINE }으로 적혀 있다.
|
||||
*
|
||||
* 시그니처 머리에서는 NEWLINE이 문법적으로 허용되고 무시된다. effects 절이
|
||||
* 줄 끝에 오면 "}"가 값 종료 토큰이라 NEWLINE이 삽입되는데, 이 자리는 문이
|
||||
* 끝날 수 있는 자리가 아니므로 아래 프로덕션이 { NEWLINE }으로 흡수한다.
|
||||
* 끝날 수 있는 자리가 아니므로 아래 프로덕션이 [ NEWLINE ]으로 흡수한다.
|
||||
* 흡수 위치를 프로덕션에 명시적으로 적는다 — 파서가 임의로 건너뛰지 않는다.
|
||||
*)
|
||||
|
||||
ident = letter , { letter | digit | "_" } ;
|
||||
int_lit = digit , { digit | "_" } ;
|
||||
string_lit = '"' , { char - '"' } , '"' ;
|
||||
string_lit = '"' , { str_char } , '"' ;
|
||||
str_char = ( char - '"' - "\" ) | escape ;
|
||||
escape = "\" , ( "n" | "t" | "\" | '"' ) ;
|
||||
bool_lit = "true" | "false" ;
|
||||
literal = int_lit | string_lit | bool_lit ;
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
(* 목록 *)
|
||||
(* ------------------------------------------------------------------ *)
|
||||
|
||||
(* 후행 콤마를 허용하는 목록. 우재귀로 적는 이유는 LL(1)이다 —
|
||||
* X , { "," , X } , [ "," ] 로 적으면 콤마를 본 시점에 "항목이 더 있는지"와
|
||||
* "이게 후행 콤마인지"가 갈리지 않는다. 콤마를 먹은 뒤 닫는 토큰을 보고
|
||||
* 갈리는 것이 파서가 실제로 하는 일이고, 아래가 그것이다. *)
|
||||
list<item> = item , list_rest<item> ;
|
||||
list_rest<item> = [ "," , [ list<item> ] ] ;
|
||||
|
||||
(* 중괄호 안의 목록. 항목 사이에 줄바꿈이 올 수 있다는 점만 다르다.
|
||||
* 항목 사이의 콤마는 필수이고 마지막 항목 뒤에서만 생략된다. *)
|
||||
brace_list<item> = [ NEWLINE ] , [ item , brace_rest<item> ] ;
|
||||
brace_rest<item> = [ NEWLINE ] ,
|
||||
[ "," , [ NEWLINE ] , [ item , brace_rest<item> ] ] ;
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
(* 모듈 *)
|
||||
(* ------------------------------------------------------------------ *)
|
||||
|
||||
module = { NEWLINE } , { item } ;
|
||||
item = ( import | reexport | decl ) , { NEWLINE } ;
|
||||
(* decl이 이미 NEWLINE을 흡수했을 수 있으므로 항목 구분자는 0개 이상이다.
|
||||
* 문 수준에서는 그렇지 않다 — stmt는 NEWLINE 하나를 반드시 요구한다 *)
|
||||
module = [ NEWLINE ] , { item } ;
|
||||
item = ( import | reexport | test_decl | decl ) , [ NEWLINE ] ;
|
||||
|
||||
import = "import" , string_lit , "as" , ident ;
|
||||
reexport = "reexport" , ident ;
|
||||
|
||||
(* 테스트는 프로그램의 일부이고 같은 검사를 받는다. pub이 없다 — 밖에서
|
||||
* 부르는 것이 아니다. 이름이 필수인 이유는 실패했을 때 무엇이 깨졌는지
|
||||
* 말해야 하기 때문이다.
|
||||
* 파라미터가 없으므로 capability를 받을 수 없고, capability를 만드는 문법도
|
||||
* 없다. 따라서 테스트는 effect-free임이 증명된다 — 관례가 아니라 검사다.
|
||||
* 그 결과 순서에 의존하지 않고, 병렬로 돌려도 같고, 캐시할 수 있다.
|
||||
* interface hash에는 들어가지 않는다 (함수 본문과 같은 이유) *)
|
||||
test_decl = "test" , string_lit , block ;
|
||||
|
||||
decl = [ "pub" ] , ( fn_decl | struct_decl | enum_decl
|
||||
| capability_decl | const_decl ) ;
|
||||
|
||||
@@ -65,53 +120,47 @@ decl = [ "pub" ] , ( fn_decl | struct_decl | enum_decl
|
||||
(* ------------------------------------------------------------------ *)
|
||||
|
||||
fn_decl = "fn" , ident , [ gen_params ] , "(" , [ params ] , ")" ,
|
||||
{ NEWLINE } ,
|
||||
[ eff_result , { NEWLINE } ] ,
|
||||
[ "->" , type , { NEWLINE } ] ,
|
||||
[ NEWLINE ] ,
|
||||
[ eff_result , [ NEWLINE ] ] ,
|
||||
[ "->" , type , [ NEWLINE ] ] ,
|
||||
[ block ] ;
|
||||
(* block이 없으면 시그니처 선언. interface 파일과 capability 본문에서 쓴다 *)
|
||||
(* block이 없으면 시그니처 선언. std/*.cool과 capability 본문에서 쓴다 *)
|
||||
|
||||
struct_decl = [ "copyable" ] , "struct" , ident , [ gen_params ] ,
|
||||
"{" , { field } , "}" ;
|
||||
field = ident , ":" , type , { NEWLINE } ,
|
||||
[ "," , { NEWLINE } ] ;
|
||||
"{" , brace_list<field> , "}" ;
|
||||
field = ident , ":" , type ;
|
||||
|
||||
enum_decl = "enum" , ident , [ gen_params ] , "{" , { variant } , "}" ;
|
||||
variant = ident , [ "(" , type_list , ")" ] , { NEWLINE } ,
|
||||
[ "," , { NEWLINE } ] ;
|
||||
enum_decl = "enum" , ident , [ gen_params ] ,
|
||||
"{" , brace_list<variant> , "}" ;
|
||||
variant = ident , [ "(" , type_list , ")" ] ;
|
||||
|
||||
capability_decl = "capability" , ident , "{" , { NEWLINE } , { cap_method } , "}" ;
|
||||
cap_method = "fn" , ident , "(" , [ params ] , ")" , { NEWLINE } ,
|
||||
[ eff_result , { NEWLINE } ] , [ "->" , type ] ,
|
||||
{ NEWLINE } ;
|
||||
capability_decl = "capability" , ident , "{" , [ NEWLINE ] , { cap_method } , "}" ;
|
||||
cap_method = "fn" , ident , [ gen_params ] , "(" , [ params ] , ")" ,
|
||||
[ NEWLINE ] ,
|
||||
[ eff_result , [ NEWLINE ] ] ,
|
||||
[ "->" , type ] ,
|
||||
[ NEWLINE ] ;
|
||||
|
||||
const_decl = "const" , ident , ":" , type , "=" , expr ;
|
||||
|
||||
gen_params = "[" , gen_param , { "," , gen_param } , [ "," ] , "]" ;
|
||||
gen_params = "[" , list<gen_param> , "]" ;
|
||||
gen_param = ident , [ ":" , "effects" ] ;
|
||||
(* ident 단독 = 타입 파라미터, ": effects" = effect 파라미터 *)
|
||||
|
||||
params = param , { "," , param } , [ "," ] ;
|
||||
params = list<param> ;
|
||||
param = [ "own" ] , [ "mut" ] , ident , ":" , type ;
|
||||
(* 무표기 = use(빌림). own만이 소유 이전을 뜻한다.
|
||||
* 수식어 순서 고정: 바인딩 수식어(own, mut)가 먼저, 타입 수식어(affine)는 type 안 *)
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
(* effect 절 *)
|
||||
(* effect *)
|
||||
(* ------------------------------------------------------------------ *)
|
||||
|
||||
(* 결과 위치 — 함수 선언 자신의 effect. 합집합 허용 *)
|
||||
eff_result = "effects" , eff_union ;
|
||||
eff_union = eff_atom , { "|" , eff_atom } ;
|
||||
(* 결과 위치에서만 합집합이 가능하다 *)
|
||||
eff_result = "effects" , eff_atom , { "|" , eff_atom } ;
|
||||
|
||||
(* 파라미터 위치 — 함수 타입 안의 effect. 합집합이 문법에 없다.
|
||||
* "검사기가 거부"가 아니라 "그런 문장이 존재하지 않음"이다 *)
|
||||
(* 파라미터 위치. 합집합이 없다 — 문법에 그런 문장이 존재하지 않는다.
|
||||
* "검사기가 거부"가 아니라 "쓸 수 없다"이다 *)
|
||||
eff_param = "effects" , eff_atom ;
|
||||
|
||||
eff_atom = ident | eff_set ;
|
||||
eff_set = "{" , { NEWLINE } ,
|
||||
[ eff_name , { { NEWLINE } , "," , { NEWLINE } , eff_name } ,
|
||||
{ NEWLINE } , [ "," , { NEWLINE } ] ] , "}" ;
|
||||
eff_set = "{" , brace_list<eff_name> , "}" ;
|
||||
eff_name = ident , "." , ident ;
|
||||
(* 타입 수준 이름만. capability 값의 identity는 정적 층에 등장하지 않는다 *)
|
||||
|
||||
@@ -121,31 +170,41 @@ eff_name = ident , "." , ident ;
|
||||
|
||||
type = fn_type | named_type ;
|
||||
|
||||
fn_type = [ "affine" ] , "fn" , "(" , [ type_list ] , ")" ,
|
||||
(* 함수 타입의 파라미터에도 own을 적는다. 이것이 없으면 "소유권을 가져가는
|
||||
* 클로저"를 타입으로 표현할 수 없고, 고차 경계에서 소유권 검사가 뚫린다 *)
|
||||
fn_type = [ "affine" ] , "fn" , "(" , [ list<fn_param_ty> ] , ")" ,
|
||||
[ eff_param ] , [ "->" , type ] ;
|
||||
fn_param_ty = [ "own" ] , type ;
|
||||
|
||||
named_type = ident , [ type_args ] ;
|
||||
type_args = "[" , targ , { "," , targ } , [ "," ] , "]" ;
|
||||
(* 다른 모듈의 타입은 별칭으로 한정한다: Shapes.Shape.
|
||||
* 한 단계뿐이다 — 별칭은 이 모듈의 이름이므로 더 이어질 자리가 없다 *)
|
||||
named_type = ident , [ "." , ident ] , [ type_args ] ;
|
||||
type_args = "[" , list<targ> , "]" ;
|
||||
targ = type | eff_set ;
|
||||
(* 제네릭 인자는 타입 또는 effect다. 맨 이름은 둘 다일 수 있으므로 파서는
|
||||
* 타입으로 읽고 이름 해소가 판정한다 — 구문 층에서 갈리지 않아도 된다 *)
|
||||
type_list = type , { "," , type } , [ "," ] ;
|
||||
type_list = list<type> ;
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
(* 문과 블록 *)
|
||||
(* ------------------------------------------------------------------ *)
|
||||
|
||||
block = "{" , { NEWLINE } ,
|
||||
[ stmt , { stmt_sep , stmt } , [ stmt_sep ] ] , "}" ;
|
||||
stmt = let_stmt | return_stmt | assign_stmt | expr ;
|
||||
stmt_sep = NEWLINE , { NEWLINE } ;
|
||||
(* 마지막 문의 구분자는 "}" 앞에서 생략된다.
|
||||
block = "{" , [ NEWLINE ] , [ stmt , stmt_rest ] , "}" ;
|
||||
stmt_rest = [ NEWLINE , [ stmt , stmt_rest ] ] ;
|
||||
(* 문 사이의 NEWLINE은 필수다. 선택적으로 적으면 식이 식 뒤에 바로 올 수
|
||||
* 있게 되고, 그러면 "-"나 "("로 시작하는 다음 문과 앞 식의 이어짐이
|
||||
* 갈리지 않는다 — 한 토큰으로 결정할 수 없게 된다.
|
||||
* 마지막 문의 구분자는 "}" 앞에서 생략된다.
|
||||
* fn(s) { String.concat(prefix, s) } 처럼 한 줄로 쓰는 자리가 있기 때문이다 *)
|
||||
|
||||
stmt = let_stmt | return_stmt | expr_stmt ;
|
||||
let_stmt = "let" , [ "mut" ] , pattern , [ ":" , type ] , "=" , expr ;
|
||||
return_stmt = "return" , [ expr ] ;
|
||||
assign_stmt = place , "=" , expr ;
|
||||
place = ident , { "." , ident } ;
|
||||
(* 값 생략은 다음 토큰이 NEWLINE이거나 "}"일 때다 *)
|
||||
|
||||
expr_stmt = expr , [ "=" , expr ] ;
|
||||
(* 대입 왼쪽에 올 수 있는 것(이름 또는 필드 접근)은 구문이 아니라 정적 검사가
|
||||
* 판정한다. 구문으로 가르면 ident 하나로 대입과 식이 갈리지 않는다 *)
|
||||
|
||||
(* 블록의 값 = 마지막 stmt가 expr이면 그 값, 아니면 Unit.
|
||||
* return은 조기 탈출 전용이며, 꼬리 위치의 return은 formatter가 지적한다 *)
|
||||
@@ -154,62 +213,98 @@ place = ident , { "." , ident } ;
|
||||
(* 식 *)
|
||||
(* ------------------------------------------------------------------ *)
|
||||
|
||||
expr = or_expr ;
|
||||
or_expr = and_expr , { "||" , and_expr } ;
|
||||
and_expr = cmp_expr , { "&&" , cmp_expr } ;
|
||||
cmp_expr = add_expr , [ cmp_op , add_expr ] ;
|
||||
cmp_op = "==" | "!=" | "<" | "<=" | ">" | ">=" ;
|
||||
add_expr = mul_expr , { ( "+" | "-" ) , mul_expr } ;
|
||||
mul_expr = unary , { ( "*" | "/" | "%" ) , unary } ;
|
||||
unary = [ "!" | "-" ] , postfix ;
|
||||
(* 매개변수 s는 이 자리에서 struct 리터럴을 쓸 수 있는지다.
|
||||
* expr = 쓸 수 있는 자리 (거의 전부)
|
||||
* expr_ns = 쓸 수 없는 자리 (if/match의 머리)
|
||||
* if/match의 머리에서 "{"가 블록의 시작인지 struct 리터럴인지 갈리지
|
||||
* 않으므로, 그 자리의 struct 리터럴은 괄호로 감싼다.
|
||||
* 제약은 머리 식의 최상위에만 걸린다 — 괄호, 대괄호, 블록, 호출 인자 등
|
||||
* 새 구문 문맥에 들어가는 순간 풀린다. 그래서 primary<no>의 괄호 안이
|
||||
* expr(= yes)이다.
|
||||
* scope의 머리에는 식이 없다(이름 둘뿐) — 제약이 걸릴 자리가 없다 *)
|
||||
expr = or_expr<yes> ;
|
||||
expr_ns = or_expr<no> ;
|
||||
|
||||
postfix = primary , { call_sfx | field_sfx | inst_sfx | "?" } ;
|
||||
or_expr<s> = and_expr<s> , { "||" , and_expr<s> } ;
|
||||
and_expr<s> = cmp_expr<s> , { "&&" , cmp_expr<s> } ;
|
||||
cmp_expr<s> = add_expr<s> , [ cmp_op , add_expr<s> ] ;
|
||||
cmp_op = "==" | "!=" | "<" | "<=" | ">" | ">=" ;
|
||||
add_expr<s> = mul_expr<s> , { ( "+" | "-" ) , mul_expr<s> } ;
|
||||
mul_expr<s> = unary<s> , { ( "*" | "/" | "%" ) , unary<s> } ;
|
||||
unary<s> = [ "!" | "-" ] , postfix<s> ;
|
||||
|
||||
postfix<s> = primary<s> , { call_sfx | field_sfx | inst_sfx | "?" } ;
|
||||
call_sfx = "(" , [ args ] , ")" ;
|
||||
field_sfx = "." , ident ;
|
||||
inst_sfx = type_args ;
|
||||
(* 후위 "[" = 명시적 인스턴스화, 전위 "[" = 리스트 리터럴. 위치가 결정한다 *)
|
||||
args = expr , { "," , expr } , [ "," ] ;
|
||||
args = list<expr> ;
|
||||
|
||||
primary = literal
|
||||
| ident
|
||||
primary<s> = literal
|
||||
| list_lit
|
||||
| struct_lit
|
||||
| closure
|
||||
| if_expr
|
||||
| match_expr
|
||||
| scope_expr
|
||||
| "(" , expr , ")" ;
|
||||
| "(" , expr , ")"
|
||||
| crash_expr
|
||||
| name_or_struct<s> ;
|
||||
|
||||
list_lit = "[" , [ expr , { "," , expr } , [ "," ] ] , "]" ;
|
||||
(* crash는 복구 불가능한 결함이다. 되돌아올 수 없으므로 타입이 Never이고,
|
||||
* Never는 어떤 타입 자리에도 놓일 수 있다 — 그래야 match 팔에서 쓸 수 있다.
|
||||
*
|
||||
* effect가 아니다. 경계 검사 하나 넣었다고 {Crash}가 호출자 전부로
|
||||
* 전염되면 effect 절은 신호가 아니라 잡음이 된다. 발산이 effect가 아닌
|
||||
* 것과 같은 이유다 — 무한 루프도 추적하지 않는다.
|
||||
*
|
||||
* 언어 수준 recover가 없다. 붙잡는 것이 있으면 그것은 예외이고, 예외는
|
||||
* 시그니처에 안 적히므로 철학 1과 충돌한다. 런타임은 격리 경계를 가질 수
|
||||
* 있다 (테스트 러너가 첫 사례).
|
||||
*
|
||||
* 되감기를 하지 않는다. crash 시 자원 해제 여부는 자원 모델과 함께
|
||||
* 결정한다 — 지금은 열어둔다.
|
||||
*
|
||||
* 키워드인 이유: prelude가 없어서 함수로 두면 쓸 때마다 import해야 한다 *)
|
||||
crash_expr = "crash" , "(" , expr , ")" ;
|
||||
|
||||
(* ident 하나로는 이름인지 struct 리터럴인지 갈리지 않는다. "{"를 보고
|
||||
* 갈리므로 왼쪽으로 인수분해해 적는다 — 파서가 실제로 하는 일이다 *)
|
||||
name_or_struct<yes> = ident , [ struct_body ] ;
|
||||
name_or_struct<no> = ident ;
|
||||
struct_body = "{" , brace_list<field_init> , "}" ;
|
||||
field_init = ident , ":" , expr ;
|
||||
|
||||
list_lit = "[" , [ args ] , "]" ;
|
||||
(* 빈 리스트는 타입 주석이 필요하다: let xs: List[Int] = [] *)
|
||||
|
||||
struct_lit = ident , "{" , { field_init } , "}" ;
|
||||
field_init = ident , ":" , expr , { NEWLINE } , [ "," , { NEWLINE } ] ;
|
||||
|
||||
closure = "fn" , "(" , [ cl_params ] , ")" ,
|
||||
[ eff_param ] , [ "->" , type ] , block ;
|
||||
cl_params = cl_param , { "," , cl_param } , [ "," ] ;
|
||||
cl_param = ident , [ ":" , type ] ;
|
||||
cl_params = list<cl_param> ;
|
||||
(* 클로저 파라미터의 소유권은 리터럴이 스스로 적는다. 타입은 기대 타입에서
|
||||
* 읽어오지만 소유권은 읽어오지 않는다 — move 검사는 타입 검사와 별도 순회라
|
||||
* 타입을 모르고, 소유권은 타입보다 결과가 크기 때문이다.
|
||||
* 무표기는 빌림이다 (함수 파라미터와 같다) *)
|
||||
cl_param = [ "own" ] , ident , [ ":" , type ] ;
|
||||
(* 파라미터 타입 생략 가능. 호출 지점의 기대 타입에서 읽어온다 — 함수 로컬이다.
|
||||
* 기대 타입이 없는 자리에서 생략하면 error *)
|
||||
|
||||
if_expr = "if" , expr_ns , block , [ "else" , ( block | if_expr ) ] ;
|
||||
match_expr = "match" , expr_ns , "{" , { arm } , "}" ;
|
||||
arm = pattern , "=>" , ( expr | block ) , { NEWLINE } ,
|
||||
[ "," , { NEWLINE } ] ;
|
||||
match_expr = "match" , expr_ns , "{" , brace_list<arm> , "}" ;
|
||||
arm = pattern , "=>" , ( expr | block ) ;
|
||||
scope_expr = "scope" , ident , "=" , ident , block ;
|
||||
(* scope 자식 = 부모 { ... }
|
||||
* 부모를 구문에 적는다. 적지 않으면 자식의 부모가 "가장 가까운 스코프"가 되어
|
||||
* 정확히 ambient authority가 된다 — 이 언어가 배제하는 것 *)
|
||||
|
||||
(* expr_ns = struct_lit로 시작하지 않는 expr.
|
||||
* if/match/scope의 머리 자리에서 "{"가 블록의 시작인지 struct 리터럴인지
|
||||
* 갈리지 않으므로, 그 자리의 struct 리터럴은 괄호로 감싼다 *)
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
(* 패턴 *)
|
||||
(* ------------------------------------------------------------------ *)
|
||||
|
||||
pattern = "_" | literal | ctor_pattern | ident ;
|
||||
ctor_pattern = ident , "(" , pattern , { "," , pattern } , [ "," ] , ")" ;
|
||||
pattern = "_" | literal | name_pattern ;
|
||||
|
||||
(* 이름 하나로는 바인딩인지 생성자인지 갈리지 않는다. 판정 규칙:
|
||||
* 한정되었으면(Shapes.Dot) 언제나 생성자다
|
||||
* 괄호가 붙으면 생성자다
|
||||
* 둘 다 아니면 바인딩이거나, 이름 해소가 생성자로 판정한다 *)
|
||||
name_pattern = ident , [ "." , ident ] , [ "(" , [ list<pattern> ] , ")" ] ;
|
||||
(* 가드 없음. 중첩은 제한 없음 (exhaustiveness 알고리즘이 처리한다) *)
|
||||
|
||||
+162
@@ -28,6 +28,16 @@ AI의 코드 생성 속도 >> 신뢰 확보 속도.
|
||||
→ "Effects: +payment.refund" 수준으로 리뷰 압축
|
||||
5. 한 개념 = 한 방식: syntax variant 최소화, 공식 formatter 하나,
|
||||
공식 toolchain 하나. 단순성의 기준은 "작성자"가 아니라 "리뷰어와 검증기"
|
||||
6. 이름은 관례가 아니라 뜻에서 고른다 — 낯섦은 한 번 치르고 끝나지만
|
||||
부정확함은 읽는 사람마다 매번 치른다
|
||||
→ 판정: 그 단어로 그것이 무엇인지 평범한 문장을 써 보라. 단어가 문장을
|
||||
도우면 맞는 이름, 문장과 싸우면 틀린 이름이다
|
||||
"크래시는 복구하는 것이 아니라 조사하는 것이다" — 돕는다
|
||||
"패닉은 복구할 수 없다" — 다른 언어에서는 할 수 있어 싸운다
|
||||
→ 널리 쓰인다는 것은 옳다는 증거가 아니다. 운영체제의 "fault"가 그렇다 —
|
||||
잘못이 아닌 것을 잘못이라 부르고, 그 오류까지 함께 전파된다
|
||||
※ 관례가 정확하면 관례를 따른다. 관례라서가 아니라 정확해서다
|
||||
※ 이름에만 적용된다. 동작을 관례와 다르게 만들 이유는 아니다
|
||||
|
||||
■ 파생 결정
|
||||
|
||||
@@ -338,6 +348,111 @@ L2 빠른 테스트 / L3 fuzzing / L4 formal proof → 요청 시, 분리 실행
|
||||
invalidation 범위를 넓히는가? / unrelated code 의미를 바꾸는가? /
|
||||
기존 개념의 중복 표현인가? → 강한 이유 없으면 거절
|
||||
|
||||
■ 실패의 세 층 — 실패 / 결함 / 감독
|
||||
서로 다른 세 질문에 답한다. 아래 층은 위 층 없이도 성립한다.
|
||||
|
||||
실패 (Result) 호출자가 대처할 수 있는가? 타입이 강제한다
|
||||
결함 (crash) 이 태스크가 계속할 수 있는가? 컴파일러는 추적하지 않는다
|
||||
감독 죽은 태스크를 어떻게 다루는가? capability가 강제한다
|
||||
|
||||
세 층이 세 가지 다른 기제로 규율된다는 것이 제대로 나뉘었다는 증거다.
|
||||
"상황이 나쁨"은 결함이 아니라 실패다 — 디스크가 가득 찬 것은 Result다.
|
||||
이 선을 긋지 않으면 crash가 게으름의 배출구가 된다.
|
||||
※ 취소는 이 셋 중 어디에도 없다. 취소는 실패가 아니라 "더 이상 필요 없음"
|
||||
이며 별도의 결정으로 남는다.
|
||||
"오류"는 컴파일러가 내는 진단에만 쓴다.
|
||||
|
||||
■ 결함 — crash
|
||||
crash(message) -> Never. 키워드다 (prelude가 없어 함수로 두면 매번 import).
|
||||
- effect가 아니다. 경계 검사 하나에 {Crash}가 호출자 전부로 전염되면
|
||||
effect 절은 신호가 아니라 잡음이 된다. 발산이 effect가 아닌 것과 같은
|
||||
이유다 — 무한 루프도 추적하지 않는다.
|
||||
- Never는 어떤 타입 자리에도 놓인다. 그래야 match 팔에서 쓸 수 있고,
|
||||
그게 없으면 crash는 식 자리에서 못 쓴다.
|
||||
- 언어 수준 recover가 없다. 붙잡는 것이 있으면 그것은 예외이고, 예외는
|
||||
시그니처에 안 적히므로 철학 1과 충돌한다.
|
||||
- 되감기를 하지 않는다. ※ crash 시 자원 해제 여부는 자원 모델과 함께
|
||||
결정한다 — 지금은 열어둔다.
|
||||
- 0으로 나누기, assert 실패, 미래의 범위·오버플로가 전부 이 하나로 모인다.
|
||||
※ crash는 프로그램의 결함을 말한다. 예상되는 실패는 Result다. 호출자가
|
||||
대처할 수 있는 것을 crash로 처리하면 오용이다.
|
||||
|
||||
■ crash는 어디까지 올라가는가 (실패의 단위)
|
||||
콜 스택의 root가 아니라 scope 트리의 root다. 즉 실패의 단위는 프로세스가
|
||||
아니라 태스크다. scope가 이미 그 모양이기 때문이다 — 렉시컬이고, 블록을
|
||||
나가는 것이 join이며, 자식이 죽었다는 사실이 부모에게 도달하는 지점이
|
||||
문법에 이미 있다(닫는 중괄호).
|
||||
|
||||
crash는 scope 트리를 타고 올라간다. 자식이 crash하면 그 scope는 join
|
||||
지점에서 crash한다. root까지 도달하면 실행이 끝난다.
|
||||
|
||||
v0는 태스크가 하나뿐이라 이 규칙이 관측되지 않는다 — "태스크가 죽는다"와
|
||||
"프로그램이 죽는다"가 같은 사건이다. 그래도 지금 적는 이유는, 동시성이
|
||||
진짜가 될 때 이 결정을 새로 하면 이미 쓰인 코드의 의미가 바뀌기 때문이다.
|
||||
|
||||
전파는 취소를 정하지 않고도 정의된다. 취소가 생기면 형제들이 언제 멈추는지가
|
||||
바뀔 뿐, scope가 실패한다는 사실은 바뀌지 않는다.
|
||||
※ 형제 취소는 여전히 미정이다. 대가를 알고 미룬다 — 취소가 없으면 형제
|
||||
하나가 끝나지 않을 때 죽은 자식의 crash가 join에 도달하지 못한다.
|
||||
실패가 hang에 가려진다. 이것이 취소가 필요한 이유이기도 하다.
|
||||
|
||||
■ 얼랭 방식은 되는가 — 감독은 되고 비구조적 spawn은 안 된다
|
||||
공유 가변 상태 없음, let it crash, 죽은 쪽의 상태를 물려받지 않음 —
|
||||
얼랭과 이미 같다. 다른 경로로 같은 결론에 도착했다.
|
||||
가져오지 않는 것: 얼랭의 비구조적 spawn(Pid가 부모보다 오래 산다).
|
||||
부모 없는 태스크는 ambient authority와 같은 문제이고, 구조적 동시성을 고른
|
||||
이유가 그것이다.
|
||||
가져오는 것: 감독. scope 트리가 이미 supervision 트리 모양이다.
|
||||
sc.spawn(f) 실패가 부모로 전파된다
|
||||
sup.spawn(sc, f) 실패가 여기서 멈춘다
|
||||
spawn은 하나의 개념이고 실패가 어디까지 가느냐가 권한으로 갈린다 — 실패를
|
||||
삼키려면 Supervisor를 받았어야 하고 그것이 시그니처에 보인다. 문법 변경 없음.
|
||||
재시작 가능한 자식은 copyable 클로저여야 한다(fn vs affine fn). 얼랭이 관례로
|
||||
지키는 것을 affinity가 이미 검사한다. 재시작 전략은 라이브러리다(얼랭도 OTP).
|
||||
※ 전제조건은 자원 모델이다. 죽은 태스크가 잡았던 것이 정리되지 않으면
|
||||
재시작이 곧 누수다.
|
||||
※ 얼랭의 보장은 BEAM의 선점 스케줄링과 프로세스별 힙 위에 선다. 우리에겐
|
||||
아직 둘 다 없다 — 이것은 설계 층의 답이지 v0가 보여줄 수 있는 것이 아니다.
|
||||
|
||||
■ 격리 경계는 recover가 아니다
|
||||
런타임은 격리 경계를 가질 수 있다. 테스트 러너가 첫 사례이고, 서버의
|
||||
요청 경계가 두 번째가 될 것이다. 구분선은 이것이다:
|
||||
|
||||
실패한 계산이 만든 값은 경계를 넘지 못한다.
|
||||
경계가 얻는 것은 "죽었다"는 사실과 메시지뿐이다.
|
||||
|
||||
recover는 같은 스택에서 재개하고 지역 변수에 접근한다 — 금지다.
|
||||
격리 경계는 죽었음을 관찰할 뿐이다 — 허용이다. Erlang의 supervisor가 죽은
|
||||
프로세스의 상태를 물려받지 않는 것과 같다.
|
||||
테스트 러너가 이 원칙대로다: 실패한 테스트에서 아무 값도 가져오지 않고
|
||||
이름과 메시지만 얻는다. 서버 경계도 언어 기능이 아니라 scope 위에 얹는다.
|
||||
|
||||
■ 내장 테스트
|
||||
test "이름" { ... }. 파라미터가 없으므로 capability를 받을 수 없고,
|
||||
capability를 만드는 문법도 없다. 따라서 effect-free임이 증명된다 —
|
||||
관례가 아니라 검사다. 그 결과:
|
||||
- 파일도 시계도 못 건드린다. 같은 입력이면 같은 결과다
|
||||
- 순서에 의존하지 않고 병렬로 돌려도 같다 → 결과를 캐시할 수 있다
|
||||
(인터페이스 해시가 안 변하면 재검사하지 않는 것과 같은 논리)
|
||||
- 자원을 가질 수 없다 (획득에 effect가 필요하므로). 그래서 "테스트가
|
||||
죽으면 자원은?"이라는 질문이 애초에 생기지 않는다
|
||||
interface hash에 들어가지 않는다 — 테스트를 고쳤다고 downstream이
|
||||
재검사되면 안 된다.
|
||||
컴파일 타임 메타프로그래밍 없음. assert는 std/test.cool에 coollang으로
|
||||
쓰인다 (crash 위의 설탕) — std에서 본문이 있는 첫 함수다. 실패 메시지에
|
||||
값이 안 나오는 것은 의도다. 표현식 텍스트를 잡으려면 매크로가 필요하다.
|
||||
※ effect 있는 코드는 테스트할 수 없다. 가짜 capability를 만드는 수단이
|
||||
없기 때문이다. 실제로 불편해진 뒤에 판단한다.
|
||||
|
||||
■ 버려지는 값
|
||||
꼬리가 아닌 자리의 식이 값을 남기면 오류다. 남긴 값은 버려지는데, 그 값이
|
||||
Result면 실패가 조용히 사라진다 — 철학 1과 정면으로 부딪힌다.
|
||||
일부러 버리려면 let _ = 로 적는다. 버린다는 사실이 코드에 보여야 한다.
|
||||
※ 개밥 먹기 2에서 발견했다. 그전까지 "이 언어에는 실패를 버릴 방법이 없다"고
|
||||
문서와 투어에 적혀 있었는데 틀린 말이었다 — List.each에 Result 반환 클로저를
|
||||
못 넘긴다는 좁은 사실을 언어 전체의 성질로 일반화한 것이었다.
|
||||
부산물로 정리 경로의 관용구가 생겼다: let _ = fs.close(f).
|
||||
|
||||
■ lint (오류다, 경고가 아니다)
|
||||
- 미사용 import: 쓰지 않는 모듈의 시그니처가 바뀌면 이 모듈이 재검사된다.
|
||||
증분 루프의 비용을 이유 없이 넓히는 선언은 남겨둘 수 없다.
|
||||
@@ -366,6 +481,53 @@ prelude는 없다. std도 명시적으로 가져온다 — 암묵적으로 끌
|
||||
것이고, 이것이 std를 "부채 상환"이 아니라 "검증"으로 본 이유다.
|
||||
결과를 버릴 방법이 언어에 없다는 성질도 여기서 처음 확인됐다.
|
||||
|
||||
■ 개밥 먹기 2 — dogfoods/ (2026-08)
|
||||
프로그램을 쓰되 돌리지 않는다. std를 선언만 두고 그것에 대고 실제 유스케이스를
|
||||
쓴다. std가 원래 선언-전용이므로 실행만 빼고 전부 진짜로 검사된다 — 종이
|
||||
스케치가 아니라 컴파일러가 검증한 설계다.
|
||||
합격 기준이 둘이다: check exit 0 + 모듈이 실제로 해소될 것. 후자가 없으면
|
||||
전자가 공허하다 — import가 조용히 해소되지 않으면 이름이 전부 불투명해져
|
||||
검사기가 무엇이든 통과시킨다. 첫 시도에서 실제로 그 일이 일어났다.
|
||||
2번(Atomic File Updater) 결과: ?를 자원과 함께 쓸 수 없다. 조기 반환이 정리를
|
||||
건너뛰는데 검사기는 통과시킨다(v0 정책: 오용 금지, 누수 허용). 제대로 정리하면
|
||||
18줄이 58줄이 되고 5단 중첩 match가 된다. resource/with가 필요한 이유가 여기
|
||||
숫자로 있다.
|
||||
6번(DB Pool)까지 두 점을 모아 resource/with를 넣기로 결정했다. 근거:
|
||||
자원을 들면 ?를 한 번도 못 쓴다(2/2), 같은 정리 패턴을 각자 발명했다(2/2),
|
||||
누수가 조용히 통과한다(2/2), 2.3~3.2배.
|
||||
고통은 자원 개수가 아니라 정리의 균일성에 비례한다 — 파일은 단계마다 정리가
|
||||
달라 4단 중첩이 되고, 풀은 언제나 반납이라 1단으로 접힌다.
|
||||
순서가 정해졌다: 먼저 수신자를 소비하는 메서드(자원이 자기 정리를 스스로
|
||||
선언할 수 있어야 한다), 그다음 resource/with. release(pool, own lease)처럼
|
||||
정리에 다른 값이 필요하면 with가 자동으로 부를 수 없기 때문이다.
|
||||
고침: 함수 타입과 클로저 파라미터의 own. 그 구멍이 std의 fold가 틀린 것을
|
||||
덮고 있었다 — 누적자는 매 단계 소비되므로 own이다.
|
||||
LRU 캐시가 제약 하나를 더했다: 축출은 렉시컬이 아니므로 with가 못 닿는다.
|
||||
따라서 자원은 렉시컬 전용으로 간다 — 자원을 담는 컨테이너(풀, 캐시,
|
||||
레지스트리)는 런타임만 만들 수 있고, 사용자는 언어로 못 만든다. 그 대가를
|
||||
알고 택한다. 이미 그렇게 되어 있었다 — std-draft의 Pool이 불투명한 런타임
|
||||
capability인 이유가 만들 수 없어서였다.
|
||||
그리고 큰 버그 하나를 잡았다: Option/Result의 패턴이 타입을 통째로 잃고
|
||||
있었다. 내장 생성자가 typecheck에 등록되지 않아 None이 변수 바인딩이 되고
|
||||
Ok(v)의 v가 TUnknown이었다. Ok(v) => v 로 Int를 String 자리에 두는 코드가
|
||||
통과했다. 소진성 검사는 자기 목록에 내장을 갖고 있어 이 사실을 덮고 있었다.
|
||||
전문은 dogfoods/FINDINGS.md.
|
||||
|
||||
■ 개밥 먹기 1 — samples/app (2026-08)
|
||||
samples/app — 설정 파서 + 리포트 도구, 2모듈 232줄. 검사기를 시험하려고
|
||||
쓴 것이 아니라 일을 하려고 쓴 첫 프로그램이다.
|
||||
결과: 되돌리기 비싼 결정은 하나도 후회되지 않았고(capability 전달, effect
|
||||
명시, 실패를 버릴 수 없음, 소진적 match), 불편은 전부 되돌리기 싼 것들이었다
|
||||
— 리스트 n번째 접근 없음, String.concat 2항. "else if가 없다"고 적은
|
||||
항목 하나는 확인해 보니 관찰자가 틀린 것이었다 — 개밥 먹기의 불편은
|
||||
언어의 성질일 수도, 쓴 사람의 습관일 수도 있다.
|
||||
전문과 증거는 docs/friction.md. 이것이 v1 설계의 첫 입력이다.
|
||||
후속: std 보강(List.first/nth/enumerate, String.join, Option/Result 함수)으로
|
||||
292줄이 232줄이 됐다. 문법은 하나도 건드리지 않았다 — 걸린 것이 전부 std의
|
||||
빈 곳이었지 문법의 문제가 아니었다는 뜻이고, 그것 자체가 결과다.
|
||||
마찰 8건 중 2건은 "언어가 못 한다"고 적었다가 확인해 보니 되는 것이었다
|
||||
(else if, enumerate). 개밥 먹기에는 확인 절차가 함께 있어야 한다.
|
||||
|
||||
■ 측정 (2026-08, v0 fast path)
|
||||
100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현.
|
||||
cold 전체 검사 245ms
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
# 개밥 먹기 발견 기록
|
||||
|
||||
`dogfoods/`를 쓰면서 나온 것들. 고친 것과 열어둔 것을 같이 적는다.
|
||||
|
||||
---
|
||||
|
||||
## 2. Atomic File Updater (2026-08-30)
|
||||
|
||||
목적: 동시성을 안 섞고 **자원 모델만** 시험한다. 임시 파일에 쓰고, 디스크에
|
||||
내리고, 원자적으로 바꿔치기한다. 실패 경로가 넷이고 전부 정리가 필요하다.
|
||||
|
||||
산출물: `std-draft/fs.cool` 65줄, `atomic-update/naive.cool` 18줄,
|
||||
`atomic-update/careful.cool` 58줄.
|
||||
|
||||
### D1. 상대 경로 import가 패키지로 오인됐다 — 고침
|
||||
|
||||
`import "../std-draft/fs"`가 조용히 해소되지 않았다. `is_package`가 첫
|
||||
세그먼트에 점이 있는지만 봤는데 `..`이 걸렸다.
|
||||
|
||||
**무서운 것은 버그 자체가 아니라 그 결과다.** import가 해소되지 않으면 그
|
||||
모듈의 이름이 전부 불투명해지고, "모르는 것을 틀렸다고 말하지 않는다"는 원칙에
|
||||
따라 검사기가 무엇이든 통과시킨다. 첫 `coolc check` 결과가 exit 0이었는데
|
||||
**아무 뜻도 없었다.** 없는 메서드를 불러도 통과했다.
|
||||
|
||||
고친 뒤 같은 코드가 정확히 잡힌다:
|
||||
|
||||
```
|
||||
capability Fs.Fs에 this_does_not_exist 메서드가 없습니다
|
||||
```
|
||||
|
||||
→ `dogfoods/README.md`의 합격 기준 ②가 여기서 나왔다.
|
||||
|
||||
### D2. 값 있는 식을 문으로 버릴 수 있었다 — 고침
|
||||
|
||||
```cool
|
||||
fs.remove(path) // Result가 조용히 사라진다
|
||||
1
|
||||
```
|
||||
|
||||
이것이 통과했다. 즉 **`Result`를 버리는 방법이 있었다.** 개밥 먹기 1차
|
||||
보고서와 언어 투어에서 내가 "이 언어에는 실패를 버릴 방법이 없다"고 적었는데
|
||||
틀렸다 — `List.each`에 Result 반환 클로저를 못 넘긴다는 좁은 사실을 언어 전체의
|
||||
성질로 일반화했다.
|
||||
|
||||
이제 꼬리가 아닌 자리의 식이 값을 남기면 오류다:
|
||||
|
||||
```
|
||||
이 식이 남기는 Result[Unit, IoError]이(가) 버려집니다
|
||||
(일부러 버리려면 let _ = 로 적으십시오)
|
||||
```
|
||||
|
||||
부산물이 좋다. 정리 경로에서 오류를 **일부러** 무시하는 관용구가 생겼고,
|
||||
버린다는 사실이 코드에 보인다:
|
||||
|
||||
```cool
|
||||
let _ = fs.close(f) // 정리 중의 실패는 삼킨다 — 원래 오류가 더 중요하다
|
||||
discard(fs, tmp, e)
|
||||
```
|
||||
|
||||
### D3. `?`를 자원과 함께 쓸 수 없다 — 이번 개밥 먹기의 본체
|
||||
|
||||
`naive.cool` 18줄은 이렇게 생겼고 **통과한다**:
|
||||
|
||||
```cool
|
||||
let f = fs.create(tmp)?
|
||||
fs.write(f, contents)? // 실패하면 f가 안 닫히고 tmp가 남는다
|
||||
fs.sync(f)?
|
||||
fs.close(f)?
|
||||
fs.rename(tmp, path)?
|
||||
```
|
||||
|
||||
`?`가 조기 반환하므로 정리를 건너뛴다. v0가 "오용 금지, 누수 허용"이라
|
||||
검사기는 아무 말도 하지 않는다.
|
||||
|
||||
제대로 정리하면 `careful.cool` 58줄이 되고 **5단 중첩 match**가 된다.
|
||||
`?`를 한 번도 못 쓴다. 같은 일에 **3.2배**다.
|
||||
|
||||
이것이 `resource` / `with` 제안이 필요한 이유의 전부다. 그리고 그 제안이
|
||||
암묵적 drop보다 나은 이유도 여기서 보인다 — 정리 지점이 닫는 중괄호로 눈에
|
||||
보이고, `Fs.close`가 effects 절에 나타나 검사된다.
|
||||
|
||||
### D4. capability 메서드가 수신자를 소비할 수 없다 — 열림
|
||||
|
||||
`close`가 핸들을 소비해야 하는데 `own`은 파라미터에만 붙는다. capability
|
||||
메서드에는 수신자를 적는 자리가 없다.
|
||||
|
||||
우회했다 — 핸들의 메서드가 아니라 `Fs`의 메서드로 두고 핸들을 인자로 받는다:
|
||||
|
||||
```cool
|
||||
fn close(own f: WriteFile) effects {Fs.close} -> Result[Unit, IoError]
|
||||
```
|
||||
|
||||
`f.close()` 대신 `fs.close(f)`가 된다. 읽기에 나쁘지 않고 오히려 권한
|
||||
(`Fs`)이 필요하다는 게 보인다. **지금은 우회로 충분해 보인다.**
|
||||
|
||||
### D5. 함수 타입에 `own`이 없다 — 열림, 그리고 이건 구멍이다
|
||||
|
||||
```cool
|
||||
pub fn apply(f: fn(own Handle) -> Handle, own h: Handle) -> Handle
|
||||
^^^ 타입이(가) 필요합니다 — own 발견
|
||||
```
|
||||
|
||||
문법이 `fn_type = "fn" "(" type_list ")"`라 `own`이 못 들어간다. 그래서
|
||||
**소유권을 가져가는 클로저를 타입으로 표현할 수 없다.**
|
||||
|
||||
결과로 이것이 통과한다:
|
||||
|
||||
```cool
|
||||
List.fold(xs, h, fn(acc, n) { step(acc, n) }) // step은 own을 받는다
|
||||
```
|
||||
|
||||
`fold`의 시그니처는 `fn(acc, a) -> acc`이고 무표기는 빌림인데, 클로저
|
||||
리터럴의 파라미터에는 소유권 표시가 없어 move 검사기가 소유한 값처럼
|
||||
취급한다. **타입은 "빌린다"고 말하는데 리터럴은 "가져간다"처럼 행동한다.**
|
||||
|
||||
기본 검사는 멀쩡하다 — 빌린 값 옮기기도 이중 소비도 정확히 잡는다.
|
||||
**고차 경계에서만 뚫린다.** Atomic Updater는 선형 코드라 안 걸렸지만,
|
||||
Mini Shell(파이프라인을 따라 FD를 나름)과 DB Pool(lease를 fold로 다룸)은
|
||||
정면으로 걸린다.
|
||||
|
||||
→ 그 둘을 쓰기 전에 결정해야 한다.
|
||||
|
||||
### D5 후속 — 고침, 그리고 std의 실수 하나가 딸려 나왔다
|
||||
|
||||
`own`을 함수 타입과 클로저 파라미터에 넣었다:
|
||||
|
||||
```cool
|
||||
fn(own Handle) -> Handle // 타입에 적을 수 있다
|
||||
List.fold(xs, h, fn(own acc, n) {...}) // 리터럴에도 적는다
|
||||
```
|
||||
|
||||
클로저 파라미터의 소유권은 **리터럴이 스스로 적는다.** 타입은 기대 타입에서
|
||||
읽어오지만 소유권은 읽어오지 않는다 — move 검사는 타입 검사와 별도 순회라
|
||||
타입을 모르고, 소유권은 타입보다 결과가 크기 때문이다.
|
||||
|
||||
`unify`가 정확히 일치를 요구한다. 빌리는 클로저를 소유 자리에 넘기는 것은
|
||||
안전하지만 그 반대는 아니고, 방향을 다루려면 부분 타입이 필요한데 없다.
|
||||
|
||||
**딸려 나온 것**: `std/list.cool`의 `fold`가 틀려 있었다.
|
||||
|
||||
```cool
|
||||
f: fn(acc, a) -> acc // 전 — 빌림
|
||||
f: fn(own acc, a) -> acc // 후 — 누적자는 매 단계 소비되고 새것으로 바뀐다
|
||||
```
|
||||
|
||||
빌림으로 적혀 있어서 **affine 값을 fold로 실어나를 수 없었다.** 그런데 그
|
||||
사실이 드러나지 않았던 이유가 바로 이 구멍이었다 — 클로저 파라미터를 무조건
|
||||
소유로 봤으니 아무 오류도 안 났다. **구멍이 자기가 숨긴 버그를 덮고 있었다.**
|
||||
|
||||
#### 남은 한계 — 제네릭을 통과해 보지 못한다
|
||||
|
||||
move 검사는 타입이 없어 `fold`의 `acc`가 호출 지점에서 무엇으로 묶이는지
|
||||
모른다. 그래서 클로저 파라미터에 표기가 없고 기대 타입이 제네릭 변수면
|
||||
affinity를 판정하지 못한다. 지금은 소유권 표기 불일치로 잡히지만, 표기가
|
||||
양쪽 다 없으면 통과한다.
|
||||
|
||||
근본 해법은 move 검사가 타입을 보는 것이고, 그건 두 순회를 합치는 일이다.
|
||||
v0에서는 하지 않는다.
|
||||
|
||||
#### 대가 하나 — own이 흔해진다
|
||||
|
||||
`own`은 흔하지 않은 쪽에 붙는 표기인데, `fold`가 항상 요구하면 흔해진다.
|
||||
`Config` 같은 copyable 누적자에도 `own`을 적게 된다. 정확히 일치를 요구한
|
||||
결과이고, 부분 타입을 넣으면 사라진다. **표기의 신호가 약해지는지 지켜본다.**
|
||||
|
||||
### D6. 문자에 접근할 방법이 없다 — 열림
|
||||
|
||||
`String`에 `split`, `trim`, `starts_with`, `contains`뿐이다. 인덱싱도
|
||||
`chars`도 없어 어휘 분석을 쓸 수 없다. Atomic Updater에는 파싱이 없어 안
|
||||
걸렸지만 Mini Shell은 여기서 막힌다.
|
||||
|
||||
되돌리기 싼 std 문제다.
|
||||
|
||||
### D7. effect-free 테스트로는 이 프로그램을 하나도 테스트할 수 없다 — 예상됨
|
||||
|
||||
`update`는 전부 effect다. 테스트는 capability를 받지 않으므로 부를 수 없다.
|
||||
설계대로이고, 대가가 이제 실물로 보인다.
|
||||
|
||||
가짜 capability를 만들 수단이 없는 한 이 층은 테스트 밖에 있다.
|
||||
※ 가짜 capability를 허용해도 보안 성질은 안 깨질 것으로 보인다 — 클로저로
|
||||
만든 가짜 `Fs`는 진짜 권한을 갖지 않는다. 실제로 불편해진 뒤에 판단한다.
|
||||
|
||||
### 소유권 검사가 실제로 잡는 것 (확인)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 두 번 닫기 | **잡힘** — `f은(는) 이미 move되었습니다 (4:22에서 소비)` |
|
||||
| 닫은 뒤 쓰기 | **잡힘** — 같은 진단 |
|
||||
| 빌린 핸들을 반환 | **잡힘** — `빌린 값이라 반환할 수 없습니다` |
|
||||
| 소유한 핸들을 반환 | 통과 — 옳다. `create`가 그렇게 생겼다 |
|
||||
| 조기 반환으로 누수 | **통과** — v0 정책("오용 금지, 누수 허용") |
|
||||
|
||||
마지막 줄이 D3이고, `with`가 닫으려는 자리다.
|
||||
|
||||
---
|
||||
|
||||
## 6. DB Connection Pool (2026-08-30)
|
||||
|
||||
목적: 자원 모델의 **두 번째 데이터 점**. 파일에 없던 셋을 압박한다 —
|
||||
자원이 두 층(풀/lease), 반납이 파괴가 아님, 획득이 실패할 수 있음.
|
||||
|
||||
산출물: `std-draft/db.cool` 50줄, `db-pool/naive.cool` 25줄,
|
||||
`db-pool/careful.cool` 58줄.
|
||||
|
||||
### D8. 인자 안에서 소비와 사용을 섞을 수 없다
|
||||
|
||||
```cool
|
||||
release_then(db, pool, l, db.query(l, sql))
|
||||
^ ^
|
||||
소비 사용
|
||||
```
|
||||
```
|
||||
l은(는) 이미 move되었습니다 (13:41에서 소비)
|
||||
```
|
||||
|
||||
move 검사가 인자를 왼쪽부터 걷기 때문이다. **실제 평가 순서와 무관하다** —
|
||||
사람은 "질의가 먼저 돌고 그다음 반납"이라고 읽지만 검사기는 그렇게 안 본다.
|
||||
|
||||
우회는 쉽다. 먼저 `let`으로 묶고 넘긴다. 다만 **왜 안 되는지가 코드에서
|
||||
안 보이므로** 진단 메시지가 위치만 말하고 이유를 말하지 않는다.
|
||||
|
||||
### 두 사례 비교 — 이것이 `with` 결정의 근거다
|
||||
|
||||
| | naive | careful | 배수 | match 중첩 | 정리 도우미 |
|
||||
|---|---|---|---|---|---|
|
||||
| Atomic Updater | 18 | 58 | **3.2** | **4단** | 2개 |
|
||||
| DB Pool | 25 | 58 | **2.3** | **1단** | 2개 |
|
||||
|
||||
**공통 (2/2)**
|
||||
|
||||
1. **자원을 들면 `?`를 한 번도 못 쓴다.** 예외 없이 그렇다
|
||||
2. **"정리하고 결과를 그대로 실어나르는 도우미"를 양쪽이 각자 발명했다**
|
||||
(`abandon`/`discard`, `release_then`/`close_then`). 같은 패턴이 두 번
|
||||
독립적으로 나온 것은 그것이 진짜 추상이라는 뜻이다
|
||||
3. **누수는 조용히 통과한다** — 검사기가 아무 말도 안 한다
|
||||
|
||||
**다른 점 — 그리고 이게 중요하다**
|
||||
|
||||
파일은 4단 중첩이고 풀은 1단이다. 이유는 **정리가 단계마다 다른가**이다.
|
||||
|
||||
- 파일: 닫기 전이면 `close + remove`, 닫은 뒤면 `remove`만 → 단계마다 다름
|
||||
- 풀: 성공이든 실패든 언제나 `release` → 하나로 접힘
|
||||
|
||||
즉 **고통이 자원 개수가 아니라 정리의 균일성에 비례한다.** `with`의 값도
|
||||
그만큼 달라진다.
|
||||
|
||||
### D9. `with`가 성립하려면 D4를 먼저 풀어야 한다
|
||||
|
||||
풀 사례가 `with`의 설계 제약 하나를 드러냈다.
|
||||
|
||||
```cool
|
||||
with l = db.acquire(pool)? { ... } // 나갈 때 무엇을 부르나?
|
||||
```
|
||||
|
||||
반납은 `db.release(pool, l)`이다 — **풀과 권한이 필요하다.** 그런데 `with`가
|
||||
자동으로 부르려면 **자원만으로 정리를 표현할 수 있어야 한다.** 즉:
|
||||
|
||||
```cool
|
||||
resource Lease {
|
||||
release close(own self) effects {Db.release} -> Result[Unit, DbError]
|
||||
}
|
||||
```
|
||||
|
||||
`own self`가 필요하다. 그런데 **capability 메서드는 수신자를 소비할 수
|
||||
없다(D4)**. `own`은 파라미터에만 붙는다.
|
||||
|
||||
파일 쪽도 같다 — `Fs.close(own f)`는 `Fs` 권한을 요구하므로 자원만으로는
|
||||
정리가 안 된다.
|
||||
|
||||
**따라서 순서가 정해진다: D4 → resource/with.** 자원이 자기 정리를 스스로
|
||||
선언할 수 있어야 `with`가 성립한다. 런타임이 lease 안에 풀을 넣어 두면
|
||||
되므로 구현상의 문제는 아니지만, **언어에 표현할 자리가 없다.**
|
||||
|
||||
### 잘 된 것 — 설계가 버틴 부분
|
||||
|
||||
- **두 층 자원이 자연스럽게 중첩된다.** 풀이 바깥, lease가 안쪽
|
||||
- **반납 ≠ 파괴가 시그니처로 표현된다.** `release(p: Pool, own l: Lease)`
|
||||
- **획득 실패가 `Result`로 자연스럽다.** `Exhausted`가 호출자에게 선택을 준다
|
||||
- `List[String]`의 `List`는 내장 타입이라 import가 필요 없다. 모듈 `List`를
|
||||
가져오는 것은 그 타입의 함수를 쓸 때뿐이다 — 미사용 import lint가 정확히
|
||||
이것을 지적했다
|
||||
|
||||
---
|
||||
|
||||
## LRU 캐시 (2026-08-30)
|
||||
|
||||
목적: 앞의 둘이 못 건드린 데를 친다. 파일과 풀은 정리 지점이 **렉시컬**이었다.
|
||||
LRU는 아니다 — **축출은 삽입의 부작용으로 예측할 수 없는 때에 일어난다.**
|
||||
|
||||
산출물: `lru-cache/values.cool` 82줄, `lru-cache/resources.cool` 73줄.
|
||||
|
||||
### D10. Option/Result 패턴이 타입을 통째로 잃고 있었다 — 고침. 큰 것이었다
|
||||
|
||||
```cool
|
||||
pub fn probe3(r: Result[Int, String]) -> String {
|
||||
match r {
|
||||
Ok(v) => v, // Int를 String 자리에 둔다
|
||||
Err(e) => e,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**통과했다.** 원인: typecheck의 `env.ctors`에 내장 생성자가 없었다. 그래서
|
||||
`None`은 생성자가 아니라 **`None`이라는 이름의 변수 바인딩**이 되고,
|
||||
`Ok(v)`의 `v`는 `TUnknown`이 되어 무엇과도 맞았다.
|
||||
|
||||
즉 **이 언어의 핵심 오류 처리 수단인 Option/Result의 패턴 매칭이 타입을 전혀
|
||||
검사하지 않고 있었다.** 소진성 검사는 자기 `is_ctor`에 내장을 갖고 있어서
|
||||
이 사실을 덮고 있었다 — 빠진 경우는 잡으면서 타입은 안 봤다.
|
||||
|
||||
내장 열거형을 등록해서 고쳤다. 기존 코드는 하나도 안 깨졌다.
|
||||
|
||||
이 버그가 여태 안 보인 이유가 씁쓸하다. `match r { Ok(v) => Int.show(v) }`
|
||||
같은 코드는 v가 TUnknown이어도 통과하므로 **아무도 이상함을 못 느낀다.**
|
||||
틀린 코드를 써봐야 드러난다.
|
||||
|
||||
### D11. struct 필드와 affine 값 — 그리고 여기서 보안 구멍이 나왔다
|
||||
|
||||
처음 이렇게 적었다: "필드 접근은 빌림이라 affine 값을 꺼낼 수 없다."
|
||||
**규칙으로는 맞았는데 구현이 그것을 강제하지 않고 있었다.**
|
||||
|
||||
나중에 "다 고쳤나"를 확인하려고 전부 다시 돌려보다 드러났다:
|
||||
|
||||
```cool
|
||||
pub capability Pay { fn charge(n: Int) effects {Pay.charge} }
|
||||
pub struct Wrapper { pay: Pay }
|
||||
pub fn consume(own p: Pay) effects {Pay.charge}
|
||||
|
||||
pub fn duplicate(w: Wrapper) effects {Pay.charge} {
|
||||
consume(w.pay)
|
||||
consume(w.pay) // 같은 capability를 두 번 소비한다
|
||||
}
|
||||
```
|
||||
|
||||
**통과했다.** 그것도 `w`가 **빌린 값**인데도. 즉 **보안 정리 (ii)("safe
|
||||
code에서 capability는 복제·위조되지 않는다")가 깨져 있었다.** 이 세션에서
|
||||
찾은 것 중 가장 심각하다.
|
||||
|
||||
원인: `E_field`가 빌린 값(`v_use = true`)을 돌려주는데, **`Move` 문맥에서
|
||||
그것을 검사하는 곳이 `E_ident` 분기에만 있었다.** 필드 접근은 그 분기를
|
||||
지나가지 않는다.
|
||||
|
||||
고쳤다. `E_field`가 Move 자리에 놓이고 필드가 affine이면 오류다:
|
||||
|
||||
```
|
||||
pay 필드는 affine이라 다른 함수에 넘길 수 없습니다
|
||||
(v0에는 부분 move가 없습니다 — 꺼내려면 열거형으로 감싸십시오)
|
||||
```
|
||||
|
||||
필드의 affinity를 알려면 바인딩의 선언 타입이 필요해서, move 검사기에
|
||||
struct 필드 표와 바인딩 타입을 넣었다. **copyable 필드는 막지 않는다** —
|
||||
`w.label`은 통과한다.
|
||||
|
||||
그리고 이것이 LRU에서 열거형으로 우회한 것을 사후에 정당화한다. 그때는
|
||||
"struct로는 안 되고 열거형으로는 된다"가 우연처럼 보였는데, **열거형이
|
||||
유일한 길인 것이 규칙이었고 struct 쪽이 새고 있었을 뿐이다.**
|
||||
|
||||
#### 이 버그가 여태 안 보인 이유
|
||||
|
||||
`samples/05_move_errors.cool`은 자원 타입을 **직접** 다룬다. struct에 넣고
|
||||
필드로 꺼내는 코드가 없었다. 개밥 먹기에서 **자원을 자료구조에 담는** 코드를
|
||||
처음 쓰면서 드러났다.
|
||||
|
||||
#### (원래 기록) 열거형으로만 둘을 함께 돌려줄 수 있다
|
||||
|
||||
`put`은 캐시와 축출된 자원을 **함께** 돌려줘야 한다. 튜플이 없으니 struct다.
|
||||
|
||||
```cool
|
||||
pub struct Put { cache: LeaseCache, evicted: Option[Db.Lease] }
|
||||
...
|
||||
match p.evicted { Some(l) => release(l), ... }
|
||||
```
|
||||
```
|
||||
l은(는) 빌린 값이라 다른 함수에 넘길 수 없습니다
|
||||
```
|
||||
|
||||
**필드 접근은 빌림이고, struct를 분해하는 패턴이 언어에 없다.** 그래서 struct에
|
||||
넣은 affine 값은 다시 꺼낼 수 없다.
|
||||
|
||||
열거형은 된다 — 패턴이 분해하기 때문이다:
|
||||
|
||||
```cool
|
||||
pub enum Put {
|
||||
Kept(LeaseCache),
|
||||
Evicted(LeaseCache, Db.Lease),
|
||||
}
|
||||
match p { Evicted(c, l) => release_then(db, pool, l, c), ... }
|
||||
```
|
||||
|
||||
**즉 "둘을 함께 돌려주기"가 열거형으로만 가능하다.** 튜플이 없는 대가가
|
||||
여기서 두 번째로 나온다(F5에 이어). 그리고 이건 취향 문제가 아니라
|
||||
**표현 가능성 문제**다.
|
||||
|
||||
→ 해법 후보: struct 분해 패턴 + 부분 이동, 또는 튜플. 둘 다 문법 결정이다.
|
||||
|
||||
### D12. 축출은 렉시컬이 아니다 — `with`가 못 닿는 첫 자리
|
||||
|
||||
`with`는 렉시컬 수명만 다룬다. 축출된 lease는 **어느 블록에도 묶이지 않는다.**
|
||||
삽입할 때 튀어나오고, 그 시점은 캐시 상태에 달려 있다.
|
||||
|
||||
그래서 `resource`가 렉시컬 전용이면 **자원을 담는 컨테이너를 언어로 만들 수
|
||||
없다.** 그리고 이것이 이미 우리 설계에 나타나 있었다 — `Pool` 자체가 자원
|
||||
캐시인데, `std-draft/db.cool`에서 **런타임이 주는 불투명한 capability**로
|
||||
선언했다. 만들 수 없어서 그렇게 한 것이다.
|
||||
|
||||
두 갈래다:
|
||||
|
||||
- **(a) 자원은 렉시컬 전용** → 자원 컨테이너는 런타임만 만들 수 있다.
|
||||
풀, 캐시, 레지스트리가 전부 언어 밖이 된다. 단순하고, 지금 상태가 그렇다
|
||||
- **(b) 자원이 자료구조로 탈출할 수 있다** → `with`만으로 부족하고 결국
|
||||
선형 타입이 필요하다. v0가 미룬 바로 그것
|
||||
|
||||
**(a)를 권한다.** 자원 컨테이너는 드물고, 만드는 쪽은 런타임이며, 쓰는 쪽은
|
||||
lease를 렉시컬하게 빌린다. 다만 **이 선택이 무엇을 포기하는지 적어둬야
|
||||
한다** — 사용자가 자기 자원 풀을 언어로 못 만든다.
|
||||
|
||||
### 자료구조 자체의 마찰 (값 판)
|
||||
|
||||
- **Map이 없어 O(n)이다.** LRU의 요점이 O(1)인데 표현할 수단이 없다
|
||||
- **튜플이 없어 운반용 struct를 세 번 만들었다** — `Got`, `Put`, `Dropping`
|
||||
- **`get`이 캐시를 새로 돌려줘야 한다.** 최근성이 바뀌므로. 공유 가변 상태가
|
||||
없다는 것의 대가이고, 호출자가 캐시를 계속 실어날라야 한다
|
||||
- **뒤에서 자르는 함수가 std에 없다.** `drop_last`를 `reverse` 두 번으로 썼다
|
||||
- **fold에 "첫 원소 건너뛰기"가 없어** `Dropping` struct를 또 만들었다
|
||||
|
||||
---
|
||||
|
||||
## 결정 — resource / with
|
||||
|
||||
**넣는다. 다만 D4가 먼저다.**
|
||||
|
||||
근거는 위 표다. 데이터 두 점에서 공통으로:
|
||||
- `?`를 자원과 함께 쓸 수 없다 (2/2)
|
||||
- 같은 정리 패턴을 각자 발명했다 (2/2)
|
||||
- 누수가 조용히 통과한다 (2/2)
|
||||
- 2.3 ~ 3.2배
|
||||
|
||||
그리고 `with`가 암묵적 drop보다 나은 이유가 여기서도 확인된다 — 정리 지점이
|
||||
닫는 중괄호로 보이고, 정리의 effect가 effects 절에 나타나 검사된다.
|
||||
|
||||
**순서**
|
||||
|
||||
1. **D4** — 수신자를 소비하는 메서드. 자원이 자기 정리를 스스로 선언할 수
|
||||
있어야 한다
|
||||
2. **`resource` 종류와 `with`** — 렉시컬 수명, 정리 강제, 전이 규칙
|
||||
(자원을 필드로 가진 타입은 전이적으로 자원)
|
||||
3. 그 뒤 Mini Shell로 세 번째 데이터 점
|
||||
|
||||
**LRU가 더한 제약**: 자원은 렉시컬 전용으로 간다. 자원을 담는 컨테이너는
|
||||
런타임만 만들 수 있다 — 축출처럼 비렉시컬한 정리는 `with`가 못 닿기 때문이다.
|
||||
사용자가 자기 자원 풀을 언어로 못 만드는 것이 그 대가다.
|
||||
|
||||
**미루는 것**: crash 시 정리 여부. `with`를 넣어도 이 질문은 열려 있다 —
|
||||
정상 종료·`return`·`?`에서는 돌고 crash에서는 안 도는 것이 지금의 잠정
|
||||
답이지만, 감독(supervision)이 들어오면 다시 봐야 한다.
|
||||
@@ -0,0 +1,67 @@
|
||||
# dogfoods — 실제 유스케이스로 언어를 압박한다
|
||||
|
||||
`samples/`가 검사기를 시험한다면 여기는 **언어가 실제 문제를 표현할 수 있는지**를
|
||||
시험한다. 성격이 다르므로 폴더를 나눈다.
|
||||
|
||||
## 방법
|
||||
|
||||
프로그램을 **쓰되 돌리지 않는다.** `std-draft/`에 런타임이 구현한다고 가정한
|
||||
선언을 두고, 그것에 대고 실제 프로그램을 쓴다.
|
||||
|
||||
"가상"이 아니다. `std/*.cool`이 원래 본문 없는 선언이므로 **실행만 빼고 전부
|
||||
진짜로 검사된다**:
|
||||
|
||||
```
|
||||
lexer ✓ parser ✓ 이름 해소 ✓ 타입 ✓ effect ✓
|
||||
capability ✓ move/affinity ✓ 소진성 ✓ interface hash ✓
|
||||
run ✗
|
||||
```
|
||||
|
||||
종이 스케치가 아니라 컴파일러가 검증한 설계다.
|
||||
|
||||
## 합격 기준
|
||||
|
||||
```
|
||||
① coolc check exit 0
|
||||
② 모듈이 실제로 해소될 것
|
||||
```
|
||||
|
||||
②가 없으면 ①이 공허하다. import가 조용히 해소되지 않으면 그 모듈의 이름이
|
||||
전부 불투명해지고, 검사기는 "모르는 것을 틀렸다고 말하지 않는다"는 원칙에 따라
|
||||
무엇이든 통과시킨다. **실제로 첫 시도에서 이 일이 일어났다** (D1 참고).
|
||||
|
||||
## 규율
|
||||
|
||||
`std-draft`의 선언은 독립적으로 정당화되어야 한다.
|
||||
|
||||
- 지금 런타임이 가진 권한으로 구현 가능할 것
|
||||
- effect를 전부 선언할 것
|
||||
- **실패 양상을 선언하는 자리에서 정할 것** — 무엇이 `Result`이고 무엇이
|
||||
`crash`인지
|
||||
- 없는 언어 기능에 기대지 말 것. 필요하다는 것이 드러나면 **그것이 발견이지
|
||||
지름길이 아니다**
|
||||
|
||||
프로그램이 예뻐 보이도록 API를 발명하면 아무것도 배우지 못한다.
|
||||
|
||||
## 한계
|
||||
|
||||
이 기준이 증명하는 것은 **표현 가능성**이지 의미론의 정확성이 아니다.
|
||||
`check`가 통과한다고 그 프로그램이 옳게 도는 것은 아니다 — 돌려본 적이 없다.
|
||||
|
||||
## 세트
|
||||
|
||||
| | | 압박하는 곳 | 상태 |
|
||||
|---|---|---|---|
|
||||
| 1 | Mini Shell | 소유권 / OS 핸들 | 대기 |
|
||||
| 2 | **Atomic File Updater** | **자원 수명, 실패 경로** | **완료** |
|
||||
| 3 | Process Supervisor | 실패 위상 | 대기 |
|
||||
| 4 | Reverse Proxy | 취소, backpressure | 대기 |
|
||||
| 5 | TCP Server | 규모의 API 모양 | 대기 |
|
||||
| 6 | DB Pool | lease, 자원 정리 | **완료** |
|
||||
| + | **LRU Cache** | **비렉시컬 정리 (축출)** | **완료** |
|
||||
| 7 | Resource Exhaustion | 실패 의미론의 최악 조건 | **미룸** — 런타임 속성이라 안 돌리면 알 수 없다. 언어 층 질문("무엇이 Result이고 무엇이 crash인가")은 std-draft 규율로 흡수했다 |
|
||||
|
||||
순서는 자원 모델(2, 6, 1) → 동시성·취소(3, 4, 5)다. 2번을 먼저 둔 이유는
|
||||
**동시성을 안 섞고 자원 모델만 시험할 수 있는 유일한 것**이기 때문이다.
|
||||
|
||||
발견은 `FINDINGS.md`에 쌓고, 확정된 결정은 `docs/thesis.md`로 올린다.
|
||||
@@ -0,0 +1,58 @@
|
||||
// 제대로 정리하는 버전.
|
||||
//
|
||||
// naive.cool과 같은 일을 하지만 모든 실패 경로에서 핸들을 닫고 임시 파일을
|
||||
// 지운다. 두 파일의 차이가 이 개밥 먹기의 산출물이다.
|
||||
|
||||
import "../std-draft/fs" as Fs
|
||||
import "cool.dev/std/string" as String
|
||||
|
||||
pub fn update(fs: Fs.Fs, path: String, contents: String)
|
||||
effects {Fs.create, Fs.write, Fs.sync, Fs.close, Fs.rename, Fs.remove}
|
||||
-> Result[Unit, Fs.IoError] {
|
||||
let tmp = String.concat(path, ".tmp")
|
||||
match fs.create(tmp) {
|
||||
Err(e) => Err(e),
|
||||
Ok(f) => finish(fs, f, tmp, path, contents),
|
||||
}
|
||||
}
|
||||
|
||||
// 핸들을 얻은 뒤. 여기서부터 모든 실패 경로가 f를 닫고 tmp를 지워야 한다.
|
||||
// ? 를 쓸 수 없다 — 조기 반환이 정리를 건너뛰기 때문이다.
|
||||
pub fn finish(
|
||||
fs: Fs.Fs,
|
||||
own f: Fs.WriteFile,
|
||||
tmp: String,
|
||||
path: String,
|
||||
contents: String,
|
||||
) effects {Fs.write, Fs.sync, Fs.close, Fs.rename, Fs.remove}
|
||||
-> Result[Unit, Fs.IoError] {
|
||||
match fs.write(f, contents) {
|
||||
Err(e) => abandon(fs, f, tmp, e),
|
||||
Ok(_) => match fs.sync(f) {
|
||||
Err(e) => abandon(fs, f, tmp, e),
|
||||
Ok(_) => match fs.close(f) {
|
||||
Err(e) => discard(fs, tmp, e),
|
||||
Ok(_) => match fs.rename(tmp, path) {
|
||||
Err(e) => discard(fs, tmp, e),
|
||||
Ok(_) => Ok(unit),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 핸들을 아직 들고 있는 실패. 닫고 지운다.
|
||||
// 정리 중의 실패는 일부러 버린다 — 원래 오류가 더 중요하다.
|
||||
// let _ = 가 버린다는 사실을 코드에 보이게 한다.
|
||||
pub fn abandon(fs: Fs.Fs, own f: Fs.WriteFile, tmp: String, e: Fs.IoError)
|
||||
effects {Fs.close, Fs.remove} -> Result[Unit, Fs.IoError] {
|
||||
let _ = fs.close(f)
|
||||
discard(fs, tmp, e)
|
||||
}
|
||||
|
||||
// 핸들은 이미 없다. 임시 파일만 지운다.
|
||||
pub fn discard(fs: Fs.Fs, tmp: String, e: Fs.IoError)
|
||||
effects {Fs.remove} -> Result[Unit, Fs.IoError] {
|
||||
let _ = fs.remove(tmp)
|
||||
Err(e)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 순진한 버전 — ?로 짧게 쓴 것.
|
||||
//
|
||||
// 이 파일의 목적은 통과하는 것이 아니라, 통과한다는 사실을 보여주는 것이다.
|
||||
|
||||
import "../std-draft/fs" as Fs
|
||||
import "cool.dev/std/string" as String
|
||||
|
||||
pub fn update(fs: Fs.Fs, path: String, contents: String)
|
||||
effects {Fs.create, Fs.write, Fs.sync, Fs.close, Fs.rename}
|
||||
-> Result[Unit, Fs.IoError] {
|
||||
let tmp = String.concat(path, ".tmp")
|
||||
let f = fs.create(tmp)?
|
||||
fs.write(f, contents)?
|
||||
fs.sync(f)?
|
||||
fs.close(f)?
|
||||
fs.rename(tmp, path)?
|
||||
Ok(unit)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// 제대로 반납하는 버전.
|
||||
//
|
||||
// Atomic Updater와 비교하는 것이 목적이다. 자원이 두 층이고, 반납이
|
||||
// 파괴가 아니며, 획득이 실패할 수 있다 — 파일에는 셋 다 없었다.
|
||||
|
||||
import "../std-draft/db" as Db
|
||||
|
||||
// 한 요청. 성공하든 실패하든 lease를 반납한다.
|
||||
pub fn load_user(db: Db.Db, pool: Db.Pool, sql: String)
|
||||
effects {Db.acquire, Db.query, Db.release} -> Result[List[String], Db.DbError] {
|
||||
match db.acquire(pool) {
|
||||
Err(e) => Err(e),
|
||||
// l을 소비하는 인자와 l을 쓰는 인자를 한 호출에 섞을 수 없다.
|
||||
// move 검사는 인자를 왼쪽부터 걷기 때문이다 — 실제 평가 순서와
|
||||
// 무관하다. 먼저 질의하고 결과를 넘긴다.
|
||||
Ok(l) => {
|
||||
let r = db.query(l, sql)
|
||||
release_then(db, pool, l, r)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 반납하고 원래 결과를 그대로 돌려준다.
|
||||
// 반납 실패는 일부러 버린다 — 원래 결과가 더 중요하다.
|
||||
//
|
||||
// 파일 때와 다른 점: 정리가 실패 경로마다 다르지 않다. 성공이든 실패든
|
||||
// 같은 일(반납)을 하므로 결과를 통째로 실어나르는 도우미 하나로 접힌다.
|
||||
pub fn release_then(
|
||||
db: Db.Db,
|
||||
pool: Db.Pool,
|
||||
own l: Db.Lease,
|
||||
r: Result[List[String], Db.DbError],
|
||||
) effects {Db.release} -> Result[List[String], Db.DbError] {
|
||||
let _ = db.release(pool, l)
|
||||
r
|
||||
}
|
||||
|
||||
// 프로그램 하나. 풀을 열고 쓰고 닫는다.
|
||||
pub fn run(db: Db.Db, url: String, sql: String)
|
||||
effects {Db.pool, Db.acquire, Db.query, Db.release, Db.close_pool}
|
||||
-> Result[List[String], Db.DbError] {
|
||||
match db.pool(url, 8) {
|
||||
Err(e) => Err(e),
|
||||
Ok(pool) => {
|
||||
let rows = load_user(db, pool, sql)
|
||||
close_then(db, pool, rows)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_then(
|
||||
db: Db.Db,
|
||||
own pool: Db.Pool,
|
||||
r: Result[List[String], Db.DbError],
|
||||
) effects {Db.close_pool} -> Result[List[String], Db.DbError] {
|
||||
let _ = db.close_pool(pool)
|
||||
r
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 순진한 버전 — ?로 짧게 쓴 것.
|
||||
//
|
||||
// Atomic Updater와 같은 병이 나오는지 본다. 다만 자원이 두 층이다 —
|
||||
// 풀과 lease. 그리고 반납은 파괴가 아니다.
|
||||
|
||||
import "../std-draft/db" as Db
|
||||
|
||||
// 한 요청. lease를 빌리고, 질의하고, 반납한다.
|
||||
pub fn load_user(db: Db.Db, pool: Db.Pool, sql: String)
|
||||
effects {Db.acquire, Db.query, Db.release} -> Result[List[String], Db.DbError] {
|
||||
let l = db.acquire(pool)?
|
||||
let rows = db.query(l, sql)? // 실패하면 lease가 반납되지 않는다
|
||||
db.release(pool, l)?
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
// 프로그램 하나. 풀을 열고 쓰고 닫는다.
|
||||
pub fn run(db: Db.Db, url: String, sql: String)
|
||||
effects {Db.pool, Db.acquire, Db.query, Db.release, Db.close_pool}
|
||||
-> Result[List[String], Db.DbError] {
|
||||
let pool = db.pool(url, 8)?
|
||||
let rows = load_user(db, pool, sql)? // 실패하면 풀도 안 닫힌다
|
||||
db.close_pool(pool)?
|
||||
Ok(rows)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// LRU 캐시 — 자원 판.
|
||||
//
|
||||
// 앞의 두 개밥(파일, 풀)은 정리 지점이 렉시컬이었다. 여기는 아니다.
|
||||
// 축출은 삽입의 부작용으로, 예측할 수 없는 때에 일어난다.
|
||||
//
|
||||
// 이것이 with 결정 전에 알아야 할 자리다 — with는 렉시컬 수명만 다루는데
|
||||
// 축출된 자원은 어느 블록에도 묶이지 않는다.
|
||||
|
||||
import "cool.dev/std/list" as List
|
||||
import "../std-draft/db" as Db
|
||||
|
||||
pub struct Held {
|
||||
key: String,
|
||||
lease: Db.Lease,
|
||||
}
|
||||
|
||||
// Lease를 필드로 가지므로 전이적으로 affine이다 — 선언하지 않아도 유도된다.
|
||||
pub struct LeaseCache {
|
||||
cap: Int,
|
||||
held: List[Held],
|
||||
}
|
||||
|
||||
// put은 캐시와 "떨어져 나온 자원"을 함께 돌려줘야 한다. 튜플이 없다.
|
||||
//
|
||||
// struct로 쓰면 막힌다 — 필드 접근은 빌림이라 affine 값을 꺼낼 수 없고,
|
||||
// struct를 분해하는 패턴이 언어에 없다. 열거형은 패턴이 분해하므로 꺼낼 수
|
||||
// 있다. 그래서 "둘을 함께 돌려주기"가 열거형으로만 가능하다.
|
||||
pub enum Put {
|
||||
Kept(LeaseCache),
|
||||
Evicted(LeaseCache, Db.Lease),
|
||||
}
|
||||
|
||||
pub fn empty(cap: Int) -> LeaseCache {
|
||||
LeaseCache { cap: cap, held: [] }
|
||||
}
|
||||
|
||||
// 넣는다. 넘치면 가장 오래된 것이 떨어져 나온다.
|
||||
//
|
||||
// 떨어져 나온 lease는 호출자가 반납해야 한다. 언어는 그것을 강제하지 못한다 —
|
||||
// Put.evicted를 무시하고 버려도 아무 말이 없다. 누수 허용이므로.
|
||||
pub fn put(own c: LeaseCache, key: String, own l: Db.Lease) -> Put
|
||||
|
||||
// 호출자가 해야 하는 일. 이 함수를 안 부르면 조용히 샌다.
|
||||
pub fn drain(db: Db.Db, pool: Db.Pool, own p: Put)
|
||||
effects {Db.release} -> LeaseCache {
|
||||
match p {
|
||||
Kept(c) => c,
|
||||
Evicted(c, l) => release_then(db, pool, l, c),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn release_then(
|
||||
db: Db.Db,
|
||||
pool: Db.Pool,
|
||||
own l: Db.Lease,
|
||||
own c: LeaseCache,
|
||||
) effects {Db.release} -> LeaseCache {
|
||||
let _ = db.release(pool, l)
|
||||
c
|
||||
}
|
||||
|
||||
// 쓰는 쪽. 매 삽입마다 축출을 받아 처리해야 한다.
|
||||
pub fn add(db: Db.Db, pool: Db.Pool, own c: LeaseCache, key: String)
|
||||
effects {Db.acquire, Db.release} -> Result[LeaseCache, Db.DbError] {
|
||||
match db.acquire(pool) {
|
||||
Err(e) => Err(e),
|
||||
Ok(l) => Ok(drain(db, pool, put(c, key, l))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(c: LeaseCache) -> Int {
|
||||
List.len(c.held)
|
||||
}
|
||||
@@ -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,51 @@
|
||||
// 데이터베이스 연결 풀 — 개밥 먹기용 초안.
|
||||
//
|
||||
// 세 층이 있고 수명을 관리하는 주체가 각각 다르다.
|
||||
// Conn 실제 연결. 풀이 소유한다. 사용자는 존재도 모른다
|
||||
// Pool 연결 묶음. 프로그램 수명. 사용자가 만들고 닫는다
|
||||
// Lease 빌린 한 자리. 요청 하나 수명. 반드시 반납해야 한다
|
||||
//
|
||||
// 사용자가 관리하는 것은 연결의 수명이 아니라 lease의 수명이다.
|
||||
// 그래서 반납은 "파괴"가 아니다 — 연결은 풀로 돌아간다.
|
||||
|
||||
// ※ List[String]의 List는 내장 타입이라 import가 필요 없다. 모듈 List를
|
||||
// 가져오는 것은 그 타입에 딸린 함수(len, map...)를 쓸 때뿐이다.
|
||||
|
||||
pub enum DbError {
|
||||
// 연결 자체가 안 된다
|
||||
Unreachable(String),
|
||||
// 풀이 가득 찼고 기다릴 수 없다. 호출자가 대처할 수 있다 — 재시도든 포기든
|
||||
Exhausted,
|
||||
// 질의가 틀렸다. 프로그램의 결함에 가깝지만 런타임 값에서 오므로 Result다
|
||||
BadQuery(String),
|
||||
Other(String),
|
||||
}
|
||||
|
||||
// 풀. capability이므로 affine이다 — 복제되지 않는다.
|
||||
pub capability Pool {
|
||||
}
|
||||
|
||||
// 빌린 한 자리. 반드시 반납해야 한다.
|
||||
// ※ 언어는 아직 그 "반드시"를 강제하지 못한다 (D3).
|
||||
pub capability Lease {
|
||||
}
|
||||
|
||||
// 데이터베이스에 손댈 권한. 풀을 만들 수 있는 유일한 출처다.
|
||||
pub capability Db {
|
||||
// 풀을 연다. size는 최대 동시 연결 수.
|
||||
fn pool(url: String, size: Int) effects {Db.pool} -> Result[Pool, DbError]
|
||||
|
||||
// 풀을 닫는다. 안에 있는 연결이 전부 정리된다.
|
||||
// 아직 빌려나간 lease가 있으면? — 아래 발견 참고.
|
||||
fn close_pool(own p: Pool) effects {Db.close_pool} -> Result[Unit, DbError]
|
||||
|
||||
// 한 자리를 빌린다. 없으면 Exhausted — 기다리지 않는다.
|
||||
// 기다리는 형태(timeout)는 취소를 정해야 해서 미룬다.
|
||||
fn acquire(p: Pool) effects {Db.acquire} -> Result[Lease, DbError]
|
||||
|
||||
// 반납한다. 연결은 풀로 돌아간다 — 파괴가 아니다.
|
||||
fn release(p: Pool, own l: Lease) effects {Db.release} -> Result[Unit, DbError]
|
||||
|
||||
// 빌린 자리로 질의한다. lease는 빌린다.
|
||||
fn query(l: Lease, sql: String) effects {Db.query} -> Result[List[String], DbError]
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// 파일 시스템 — 개밥 먹기용 초안.
|
||||
//
|
||||
// 이 파일은 구현이 아니라 계약이다. 런타임이 구현한다고 가정하고, coollang이
|
||||
// 이 일을 표현할 수 있는지만 본다. 실행은 안 되지만 타입·effect·capability·
|
||||
// 소유권 검사는 전부 진짜로 돈다.
|
||||
//
|
||||
// 규율: 여기 적는 것은 지금 런타임이 가진 권한으로 구현 가능해야 하고,
|
||||
// effect를 전부 선언해야 하며, 없는 언어 기능에 기대면 안 된다. 없는 기능이
|
||||
// 필요하다는 게 드러나면 그것이 발견이지 지름길이 아니다.
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 실패는 무엇인가
|
||||
//
|
||||
// 여기서 Result와 crash의 선을 긋는다. 기준은 "호출자가 대처할 수 있는가"다.
|
||||
// 대처할 수 있다 → Result
|
||||
// 프로그램이 틀렸다 → crash
|
||||
//
|
||||
// 그래서 아래는 전부 Result다. 파일이 없는 것도, 권한이 없는 것도, 디스크가
|
||||
// 가득 찬 것도 프로그램의 결함이 아니다 — 세상의 상태다.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
pub enum IoError {
|
||||
NotFound(String),
|
||||
Denied(String),
|
||||
Exists(String),
|
||||
NoSpace(String),
|
||||
// 나머지. 런타임이 분류하지 못한 것들
|
||||
Other(String),
|
||||
}
|
||||
|
||||
// 열린 쓰기 핸들.
|
||||
//
|
||||
// capability로 선언하는 이유가 둘이다.
|
||||
// 1. 이것은 실제로 권한이다 — 이 파일에 쓸 수 있는 권한
|
||||
// 2. capability는 affinity의 뿌리이므로 복제되지 않는다
|
||||
// 메서드가 없다. 핸들로 할 수 있는 일은 Fs를 통해서 한다 — 아래 참고.
|
||||
pub capability WriteFile {
|
||||
}
|
||||
|
||||
// 파일 시스템에 손댈 권한.
|
||||
//
|
||||
// 핸들의 메서드가 아니라 Fs의 메서드로 둔 이유: close가 핸들을 소비해야
|
||||
// 하는데, capability 메서드는 수신자를 소비할 방법이 없다. own은 파라미터에만
|
||||
// 붙는다. 그래서 핸들을 인자로 받는 형태가 된다.
|
||||
// ※ 발견 1: capability 메서드가 수신자를 소비할 수 없다.
|
||||
pub capability Fs {
|
||||
// 새로 만든다. 이미 있으면 자른다.
|
||||
fn create(path: String) effects {Fs.create} -> Result[WriteFile, IoError]
|
||||
|
||||
// 핸들을 빌린다. 여러 번 쓸 수 있다.
|
||||
fn write(f: WriteFile, s: String) effects {Fs.write} -> Result[Unit, IoError]
|
||||
|
||||
// 디스크까지 내려간다. 이게 없으면 rename이 원자적이어도 내용이 없을 수 있다.
|
||||
fn sync(f: WriteFile) effects {Fs.sync} -> Result[Unit, IoError]
|
||||
|
||||
// 핸들을 소비한다. 두 번 닫을 수 없다 — own이 그것을 강제한다.
|
||||
fn close(own f: WriteFile) effects {Fs.close} -> Result[Unit, IoError]
|
||||
|
||||
// 같은 파일 시스템 안에서 원자적이다.
|
||||
fn rename(from: String, to: String) effects {Fs.rename} -> Result[Unit, IoError]
|
||||
|
||||
fn remove(path: String) effects {Fs.remove} -> Result[Unit, IoError]
|
||||
|
||||
fn read(path: String) effects {Fs.read} -> Result[String, IoError]
|
||||
}
|
||||
+29
-6
@@ -21,7 +21,8 @@ type ty =
|
||||
}
|
||||
| T_fn of {
|
||||
affine : bool;
|
||||
params : ty list;
|
||||
(* 파라미터마다 소유권 표시. 무표기는 빌림 *)
|
||||
params : fn_param_ty list;
|
||||
eff : eff_atom option;
|
||||
ret : ty option;
|
||||
pos : pos;
|
||||
@@ -29,6 +30,7 @@ type ty =
|
||||
|
||||
(* 제네릭 인자는 타입 또는 effect다. 맨 이름은 둘 다일 수 있으므로
|
||||
파서는 타입으로 읽고 이름 해소가 판정한다. *)
|
||||
and fn_param_ty = { pt_own : bool; pt_ty : ty }
|
||||
and targ = TA_ty of ty | TA_eff of eff_atom
|
||||
|
||||
type pattern =
|
||||
@@ -74,16 +76,21 @@ type expr =
|
||||
| E_inst of { callee : expr; args : targ list; pos : pos }
|
||||
| E_try of { inner : expr; pos : pos }
|
||||
| E_unary of { op : unop; operand : expr; pos : pos }
|
||||
(* 복구 불가능한 실패. 타입은 Never — 돌아오지 않으므로 어떤 자리에도 놓인다 *)
|
||||
| E_crash of { msg : expr; pos : pos }
|
||||
| E_binary of { op : binop; lhs : expr; rhs : expr; pos : pos }
|
||||
|
||||
and closure = {
|
||||
cl_params : (string * ty option) list;
|
||||
(* (own, 이름, 타입). 타입은 기대 타입에서 읽어오지만 소유권은 리터럴이
|
||||
스스로 적는다 — move 검사가 타입을 모르기 때문이다 *)
|
||||
cl_params : cl_param list;
|
||||
cl_eff : eff_atom option;
|
||||
cl_ret : ty option;
|
||||
cl_body : block;
|
||||
cl_pos : pos;
|
||||
}
|
||||
|
||||
and cl_param = { cp_own : bool; cp_name : string; cp_ty : ty option }
|
||||
and arm = { arm_pat : pattern; arm_body : expr; arm_pos : pos }
|
||||
and block = { stmts : stmt list; block_pos : pos }
|
||||
|
||||
@@ -148,6 +155,9 @@ type item =
|
||||
pos : pos;
|
||||
}
|
||||
| I_const of { pub : bool; name : string; ty : ty; value : expr; pos : pos }
|
||||
(* 테스트. 파라미터가 없으므로 capability를 받을 수 없고, 그래서
|
||||
effect-free임이 증명된다 — 관례가 아니라 검사다 *)
|
||||
| I_test of { name : string; body : block; pos : pos }
|
||||
|
||||
type modul = { items : item list }
|
||||
|
||||
@@ -189,7 +199,11 @@ let rec buf_ty b = function
|
||||
Buffer.add_char b ')')
|
||||
| T_fn { affine; params; eff; ret; _ } ->
|
||||
Buffer.add_string b (if affine then "(affine-fn (" else "(fn (");
|
||||
buf_list b (buf_ty b) " " params;
|
||||
buf_list b
|
||||
(fun (p : fn_param_ty) ->
|
||||
if p.pt_own then Buffer.add_string b "own ";
|
||||
buf_ty b p.pt_ty)
|
||||
" " params;
|
||||
Buffer.add_char b ')';
|
||||
(match eff with
|
||||
| None -> ()
|
||||
@@ -244,6 +258,10 @@ let binop_name = function
|
||||
|
||||
let rec buf_expr b = function
|
||||
| E_lit (l, _) -> buf_lit b l
|
||||
| E_crash { msg; _ } ->
|
||||
Buffer.add_string b "(crash ";
|
||||
buf_expr b msg;
|
||||
Buffer.add_char b ')'
|
||||
| E_ident (n, _) -> Buffer.add_string b n
|
||||
| E_list (xs, _) ->
|
||||
Buffer.add_string b "(list";
|
||||
@@ -265,9 +283,10 @@ let rec buf_expr b = function
|
||||
| E_closure c ->
|
||||
Buffer.add_string b "(closure (";
|
||||
buf_list b
|
||||
(fun (n, t) ->
|
||||
Buffer.add_string b n;
|
||||
match t with
|
||||
(fun (p : cl_param) ->
|
||||
if p.cp_own then Buffer.add_string b "own ";
|
||||
Buffer.add_string b p.cp_name;
|
||||
match p.cp_ty with
|
||||
| None -> ()
|
||||
| Some t ->
|
||||
Buffer.add_char b ':';
|
||||
@@ -468,6 +487,10 @@ let buf_item b = function
|
||||
Buffer.add_char b ' ';
|
||||
buf_expr b value;
|
||||
Buffer.add_char b ')'
|
||||
| I_test { name; body; _ } ->
|
||||
Buffer.add_string b ("(test \"" ^ name ^ "\" ");
|
||||
buf_block b body;
|
||||
Buffer.add_char b ')'
|
||||
|
||||
let show_item item =
|
||||
let b = Buffer.create 256 in
|
||||
|
||||
+652
@@ -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;
|
||||
|
||||
+219
-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,121 @@ let root_capability name : value option =
|
||||
Buffer.add_char out '\n';
|
||||
VUnit );
|
||||
] ))
|
||||
| "File" ->
|
||||
Some
|
||||
(VCap
|
||||
( "File",
|
||||
[
|
||||
( "read",
|
||||
fun args ->
|
||||
match args with
|
||||
| [ VStr path ] -> (
|
||||
try VEnum ("Result", "Ok", [ VStr (read_whole path) ])
|
||||
with Sys_error m -> VEnum ("Result", "Err", [ VStr m ]))
|
||||
| _ -> VEnum ("Result", "Err", [ VStr "read: 경로가 필요합니다" ]) );
|
||||
] ))
|
||||
| "Args" ->
|
||||
Some
|
||||
(VCap
|
||||
( "Args",
|
||||
[ ("all", fun _ -> VList (List.map (fun s -> VStr s) !argv)) ] ))
|
||||
| "TaskScope" ->
|
||||
(* 루트 스코프. 구조적 동시성의 뿌리도 런타임이 준다 — 프로그램이
|
||||
스스로 만들 수 있으면 부모 없는 작업이 생긴다. *)
|
||||
Some (VScope "root")
|
||||
| _ -> None
|
||||
|
||||
(* 런타임이 구현한 이름 전부. std/*.cool의 선언과 이 목록이 어긋나면 검사는
|
||||
통과하고 실행이 죽는다 — 그 간극을 테스트가 막는다 (docs/friction.md F7).
|
||||
v1에서 std를 coollang으로 구현하면 이 목록 자체가 사라진다. *)
|
||||
let implemented =
|
||||
[
|
||||
"string.len";
|
||||
"string.is_empty";
|
||||
"string.concat";
|
||||
"string.split";
|
||||
"string.join";
|
||||
"string.trim";
|
||||
"string.starts_with";
|
||||
"string.contains";
|
||||
"int.show";
|
||||
"int.abs";
|
||||
"int.parse";
|
||||
"bool.show";
|
||||
"list.len";
|
||||
"list.is_empty";
|
||||
"list.first";
|
||||
"list.nth";
|
||||
"list.push";
|
||||
"list.concat";
|
||||
"list.reverse";
|
||||
"list.enumerate";
|
||||
"list.each";
|
||||
"list.map";
|
||||
"list.filter";
|
||||
"list.fold";
|
||||
"option.is_some";
|
||||
"option.map";
|
||||
"option.unwrap_or";
|
||||
"option.ok_or";
|
||||
"result.is_ok";
|
||||
"result.map";
|
||||
"result.map_err";
|
||||
"result.unwrap_or";
|
||||
]
|
||||
|
||||
let builtin pos name (args : value list) : value =
|
||||
match (name, args) with
|
||||
| "string.concat", [ VStr a; VStr b ] -> VStr (a ^ b)
|
||||
| "string.len", [ VStr a ] -> VInt (String.length a)
|
||||
| "string.is_empty", [ VStr a ] -> VBool (a = "")
|
||||
| "string.split", [ VStr s; VStr sep ] ->
|
||||
VList (List.map (fun x -> VStr x) (split_on s sep))
|
||||
| "string.trim", [ VStr s ] -> VStr (String.trim s)
|
||||
| "string.starts_with", [ VStr s; VStr p ] ->
|
||||
VBool
|
||||
(String.length s >= String.length p
|
||||
&& String.sub s 0 (String.length p) = p)
|
||||
| "string.contains", [ VStr s; VStr n ] -> VBool (find_sub s n <> None)
|
||||
| "int.show", [ VInt n ] -> VStr (string_of_int n)
|
||||
| "int.abs", [ VInt n ] -> VInt (abs n)
|
||||
| "int.parse", [ VStr s ] -> (
|
||||
match int_of_string_opt (String.trim s) with
|
||||
| Some n -> VEnum ("Result", "Ok", [ VInt n ])
|
||||
| None -> VEnum ("Result", "Err", [ VStr (s ^ "은(는) 정수가 아닙니다") ]))
|
||||
| "bool.show", [ VBool b ] -> VStr (if b then "true" else "false")
|
||||
| "list.len", [ VList xs ] -> VInt (List.length xs)
|
||||
| "list.is_empty", [ VList xs ] -> VBool (xs = [])
|
||||
| "list.push", [ VList xs; x ] -> VList (xs @ [ x ])
|
||||
| "list.concat", [ VList xs; VList ys ] -> VList (xs @ ys)
|
||||
| "list.reverse", [ VList xs ] -> VList (List.rev xs)
|
||||
| "list.first", [ VList xs ] -> (
|
||||
match xs with
|
||||
| [] -> VEnum ("Option", "None", [])
|
||||
| x :: _ -> VEnum ("Option", "Some", [ x ]))
|
||||
| "list.enumerate", [ VList xs ] ->
|
||||
VList
|
||||
(List.mapi
|
||||
(fun i x ->
|
||||
VStruct ("List.Indexed", [ ("i", ref (VInt i)); ("value", ref x) ]))
|
||||
xs)
|
||||
| "list.nth", [ VList xs; VInt i ] -> (
|
||||
match List.nth_opt xs i with
|
||||
| Some x -> VEnum ("Option", "Some", [ x ])
|
||||
| None -> VEnum ("Option", "None", []))
|
||||
| "string.join", [ VStr sep; VList parts ] ->
|
||||
VStr
|
||||
(String.concat sep
|
||||
(List.map (function VStr s -> s | v -> show v) parts))
|
||||
| "option.is_some", [ VEnum ("Option", v, _) ] -> VBool (v = "Some")
|
||||
| "option.unwrap_or", [ VEnum ("Option", "Some", [ x ]); _ ] -> x
|
||||
| "option.unwrap_or", [ _; fallback ] -> fallback
|
||||
| "option.ok_or", [ VEnum ("Option", "Some", [ x ]); _ ] ->
|
||||
VEnum ("Result", "Ok", [ x ])
|
||||
| "option.ok_or", [ _; e ] -> VEnum ("Result", "Err", [ e ])
|
||||
| "result.is_ok", [ VEnum ("Result", v, _) ] -> VBool (v = "Ok")
|
||||
| "result.unwrap_or", [ VEnum ("Result", "Ok", [ x ]); _ ] -> x
|
||||
| "result.unwrap_or", [ _; fallback ] -> fallback
|
||||
| _ ->
|
||||
fail pos (Printf.sprintf "%s은(는) 런타임이 제공하지 않습니다 (표준 라이브러리가 아직 없습니다)" name)
|
||||
|
||||
@@ -181,6 +320,8 @@ let rec eval st (env : env) (e : Ir.t) : value =
|
||||
| [ f ] -> apply st pos f []
|
||||
| _ -> fail pos "spawn은 함수 하나를 받습니다")
|
||||
| other -> fail pos (Printf.sprintf "%s에는 필드가 없습니다" (show other)))
|
||||
| Ir.I_crash (msg, pos) -> (
|
||||
match eval st env msg with VStr s -> fail pos s | v -> fail pos (show v))
|
||||
| Ir.I_unary (op, e, pos) -> (
|
||||
match (op, eval st env e) with
|
||||
| Ast.U_not, VBool b -> VBool (not b)
|
||||
@@ -228,6 +369,38 @@ and apply st pos f args =
|
||||
match args with
|
||||
| [ VList xs; f ] -> VList (List.map (fun x -> apply st pos f [ x ]) xs)
|
||||
| _ -> fail pos "list.map은 리스트와 함수를 받습니다")
|
||||
| VBuiltin "list.filter" -> (
|
||||
match args with
|
||||
| [ VList xs; f ] ->
|
||||
VList
|
||||
(List.filter
|
||||
(fun x ->
|
||||
match apply st pos f [ x ] with VBool b -> b | _ -> false)
|
||||
xs)
|
||||
| _ -> fail pos "list.filter는 리스트와 함수를 받습니다")
|
||||
| VBuiltin "option.map" -> (
|
||||
match args with
|
||||
| [ VEnum ("Option", "Some", [ x ]); f ] ->
|
||||
VEnum ("Option", "Some", [ apply st pos f [ x ] ])
|
||||
| [ o; _ ] -> o
|
||||
| _ -> fail pos "option.map은 Option과 함수를 받습니다")
|
||||
| VBuiltin "result.map" -> (
|
||||
match args with
|
||||
| [ VEnum ("Result", "Ok", [ x ]); f ] ->
|
||||
VEnum ("Result", "Ok", [ apply st pos f [ x ] ])
|
||||
| [ r; _ ] -> r
|
||||
| _ -> fail pos "result.map은 Result와 함수를 받습니다")
|
||||
| VBuiltin "result.map_err" -> (
|
||||
match args with
|
||||
| [ VEnum ("Result", "Err", [ e ]); f ] ->
|
||||
VEnum ("Result", "Err", [ apply st pos f [ e ] ])
|
||||
| [ r; _ ] -> r
|
||||
| _ -> fail pos "result.map_err는 Result와 함수를 받습니다")
|
||||
| VBuiltin "list.fold" -> (
|
||||
match args with
|
||||
| [ VList xs; init; f ] ->
|
||||
List.fold_left (fun acc x -> apply st pos f [ acc; x ]) init xs
|
||||
| _ -> fail pos "list.fold는 리스트, 초기값, 함수를 받습니다")
|
||||
| VBuiltin n -> builtin pos n args
|
||||
| VNative f -> f args
|
||||
| other -> fail pos (Printf.sprintf "%s은(는) 부를 수 없습니다" (show other))
|
||||
@@ -313,13 +486,17 @@ and eval_binary st env op a b pos =
|
||||
|
||||
(* main이 선언한 capability만 런타임이 넘긴다. 선언하지 않은 권한은
|
||||
프로그램 안에 존재하지 않는다. *)
|
||||
let run (prog : Ir.program) (entry : string)
|
||||
(main_params : (string * string) list) : (string, Token.pos * string) result
|
||||
=
|
||||
(* 실패해도 그때까지 나온 출력을 함께 돌려준다.
|
||||
print는 실제로 일어난 effect다. 일어난 일을 안 보여주면 "어디까지 갔나"를
|
||||
알 수 없고, 그게 실패했을 때 가장 먼저 보고 싶은 것이다. *)
|
||||
let run ?(args = []) (prog : Ir.program) (entry : string)
|
||||
(main_params : (string * string) list) :
|
||||
string * (Token.pos * string) option =
|
||||
Buffer.clear out;
|
||||
argv := args;
|
||||
let st = { prog } in
|
||||
match Hashtbl.find_opt prog.Ir.fns (entry ^ "#main") with
|
||||
| None -> Error (Token.{ line = 0; col = 0 }, "main 함수가 없습니다")
|
||||
| None -> ("", Some (Token.{ line = 0; col = 0 }, "main 함수가 없습니다"))
|
||||
| Some fn -> (
|
||||
let args =
|
||||
List.map
|
||||
@@ -331,12 +508,46 @@ let run (prog : Ir.program) (entry : string)
|
||||
main_params
|
||||
in
|
||||
match List.find_opt Result.is_error args with
|
||||
| Some (Error m) -> Error (Token.{ line = 0; col = 0 }, m)
|
||||
| Some (Error m) -> ("", Some (Token.{ line = 0; col = 0 }, m))
|
||||
| _ -> (
|
||||
let args = List.map Result.get_ok args in
|
||||
try
|
||||
ignore (apply st Token.{ line = 0; col = 0 } (VFn fn) args);
|
||||
Ok (Buffer.contents out)
|
||||
(Buffer.contents out, None)
|
||||
with
|
||||
| Fail (pos, msg) -> Error (pos, msg)
|
||||
| Return_exc _ -> Ok (Buffer.contents out)))
|
||||
| Fail (pos, msg) -> (Buffer.contents out, Some (pos, msg))
|
||||
| Return_exc _ -> (Buffer.contents out, None)))
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
(* 테스트 러너 *)
|
||||
(* *)
|
||||
(* 격리는 런타임의 일이지 언어의 일이 아니다. 언어에는 recover가 없고, *)
|
||||
(* 러너는 죽은 것을 되살리는 것이 아니라 죽었다는 사실을 관찰하고 다음 *)
|
||||
(* 으로 간다 — 프로세스 경계에 가깝다. *)
|
||||
(* *)
|
||||
(* 테스트가 effect-free임은 검사기가 이미 보장한다. 그래서 순서에 *)
|
||||
(* 의존하지 않고, 어떤 순서로 돌려도 같다. *)
|
||||
(* ------------------------------------------------------------------ *)
|
||||
|
||||
type test_result = {
|
||||
t_module : string;
|
||||
t_name : string;
|
||||
t_pos : Token.pos;
|
||||
t_failure : (Token.pos * string) option;
|
||||
}
|
||||
|
||||
let run_tests ?(filter = "") (prog : Ir.program) : test_result list =
|
||||
let st = { prog } in
|
||||
List.rev prog.Ir.tests
|
||||
|> List.filter (fun (_, name, _, _) ->
|
||||
filter = "" || find_sub name filter <> None)
|
||||
|> List.map (fun (m, name, pos, body) ->
|
||||
let failure =
|
||||
try
|
||||
ignore (eval st [ [] ] body);
|
||||
None
|
||||
with
|
||||
| Fail (p, msg) -> Some (p, msg)
|
||||
| Return_exc _ -> None
|
||||
in
|
||||
{ t_module = m; t_name = name; t_pos = pos; t_failure = failure })
|
||||
|
||||
@@ -45,6 +45,7 @@ type t =
|
||||
| I_seq of stmt list * t (* 블록: 문 나열 + 꼬리 값 *)
|
||||
| I_call of { callee : t; args : t list; pos : pos }
|
||||
| I_field of { obj : t; name : string; pos : pos }
|
||||
| I_crash of t * pos
|
||||
| I_unary of Ast.unop * t * pos
|
||||
| I_binary of Ast.binop * t * t * pos
|
||||
|
||||
@@ -60,6 +61,8 @@ type fn = { fn_name : string; fn_params : string list; fn_body : t }
|
||||
|
||||
type program = {
|
||||
fns : (string, fn) Hashtbl.t;
|
||||
(* 테스트. (모듈 경로, 이름, 위치, 본문) *)
|
||||
mutable tests : (string * string * pos * t) list;
|
||||
(* variant 이름 -> (enum 이름, 인자 개수) *)
|
||||
ctors : (string, string * int) Hashtbl.t;
|
||||
caps : (string, string list) Hashtbl.t; (* capability -> 메서드 이름 *)
|
||||
@@ -183,6 +186,7 @@ let rec lower_pat c (p : Ast.pattern) : pat =
|
||||
|
||||
let rec lower c (e : Ast.expr) : t =
|
||||
match e with
|
||||
| Ast.E_crash { msg; pos } -> I_crash (lower c msg, pos)
|
||||
| Ast.E_lit (l, _) -> I_lit l
|
||||
| Ast.E_ident (n, pos) -> I_ref (classify c n, pos)
|
||||
| Ast.E_list (xs, _) -> I_list (List.map (lower c) xs)
|
||||
@@ -190,12 +194,12 @@ let rec lower c (e : Ast.expr) : t =
|
||||
I_make (name, List.map (fun (n, e) -> (n, lower c e)) fields)
|
||||
| Ast.E_closure cl ->
|
||||
lpush c;
|
||||
List.iter (fun (n, _) -> lbind c n) cl.cl_params;
|
||||
List.iter (fun (p : Ast.cl_param) -> lbind c p.cp_name) cl.cl_params;
|
||||
let body = lower_block c cl.cl_body in
|
||||
lpop c;
|
||||
I_closure
|
||||
{
|
||||
c_params = List.map fst cl.cl_params;
|
||||
c_params = List.map (fun (p : Ast.cl_param) -> p.cp_name) cl.cl_params;
|
||||
c_body = body;
|
||||
c_pos = cl.cl_pos;
|
||||
}
|
||||
@@ -310,6 +314,7 @@ let of_program (mods : modinfo list) : program =
|
||||
let prog =
|
||||
{
|
||||
fns = Hashtbl.create 64;
|
||||
tests = [];
|
||||
ctors = Hashtbl.create 64;
|
||||
caps = Hashtbl.create 16;
|
||||
consts = Hashtbl.create 16;
|
||||
@@ -377,6 +382,12 @@ let of_program (mods : modinfo list) : program =
|
||||
| Ast.I_const { name; value; _ } ->
|
||||
c.locals <- [];
|
||||
Hashtbl.replace prog.consts (key name) (lower c value)
|
||||
| Ast.I_test { name; body; pos } ->
|
||||
c.locals <- [];
|
||||
lpush c;
|
||||
let b = lower_block c body in
|
||||
lpop c;
|
||||
prog.tests <- (mi.m_path, name, pos, b) :: prog.tests
|
||||
| _ -> ())
|
||||
mi.m_ast.items)
|
||||
mods;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
(* 문법 문서의 어휘 절을 코드에서 생성한다.
|
||||
|
||||
키워드 표와 "줄을 끝낼 수 있는 토큰" 목록은 지금까지 token.ml과
|
||||
grammar.ebnf 양쪽에 손으로 적혀 있었다. 그런 목록은 어긋난다 —
|
||||
실제로 이 프로젝트의 문법 문서 전체가 그렇게 어긋났다.
|
||||
|
||||
여기서 생성하고 테스트가 대조하므로, 이제 토큰을 추가하면 문서가 낡거나
|
||||
빌드가 깨진다. 사람이 두 곳을 맞출 의무가 없다. *)
|
||||
|
||||
let display (k : Token.kind) =
|
||||
match k with
|
||||
| Token.Ident _ -> Some "ident"
|
||||
| Token.Int _ -> Some "int_lit"
|
||||
| Token.Str _ -> Some "string_lit"
|
||||
| Token.Newline | Token.Eof -> None
|
||||
| _ -> Some ("\"" ^ Token.show_kind k ^ "\"")
|
||||
|
||||
let is_keyword (k : Token.kind) = Token.keyword (Token.show_kind k) = Some k
|
||||
|
||||
let keywords () =
|
||||
List.filter_map
|
||||
(fun k ->
|
||||
if is_keyword k then Some ("\"" ^ Token.show_kind k ^ "\"") else None)
|
||||
Token.all_kinds
|
||||
|
||||
let statement_enders () =
|
||||
List.filter_map
|
||||
(fun k -> if Token.can_end_statement k then display k else None)
|
||||
Token.all_kinds
|
||||
|
||||
let begin_mark = "(* 여기부터 lib/token.ml에서 생성됩니다 — 손으로 고치지 마십시오 *)"
|
||||
|
||||
let end_mark = "(* 생성 끝 *)"
|
||||
|
||||
(* 한 줄이 길어지지 않게 접는다. 구분자는 줄 끝에 남겨 이어짐이 보이게 한다 *)
|
||||
let wrap ~indent items =
|
||||
let rec go acc line = function
|
||||
| [] -> List.rev (line :: acc)
|
||||
| x :: rest ->
|
||||
if line = indent then go acc (line ^ x) rest
|
||||
else if String.length line + String.length x + 3 > 72 then
|
||||
go ((line ^ " |") :: acc) (indent ^ x) rest
|
||||
else go acc (line ^ " | " ^ x) rest
|
||||
in
|
||||
String.concat "\n" (go [] indent items)
|
||||
|
||||
let render () =
|
||||
String.concat "\n"
|
||||
[
|
||||
begin_mark;
|
||||
" * 줄을 끝낼 수 있는 토큰. 줄의 마지막 토큰이 이 중 하나이면 그 줄 끝에";
|
||||
" * NEWLINE이 삽입된다. Token.can_end_statement가 원본이다.";
|
||||
" *";
|
||||
wrap ~indent:" * " (statement_enders ());
|
||||
" *";
|
||||
" * 키워드. 이름으로 쓸 수 없다. Token.keyword가 원본이다.";
|
||||
" *";
|
||||
wrap ~indent:" * " (keywords ());
|
||||
" " ^ end_mark;
|
||||
]
|
||||
+91
-15
@@ -27,12 +27,17 @@ type binding = {
|
||||
b_use : bool;
|
||||
b_mut : bool;
|
||||
b_depth : int;
|
||||
(* 선언된 타입. 필드가 affine인지 알려면 필요하다 — 모르면 필드에서
|
||||
꺼내는 것을 막을 수 없고, 그러면 capability가 struct를 통해 복제된다. *)
|
||||
b_ty : ty option;
|
||||
}
|
||||
|
||||
type fninfo = { f_params : param list; f_ret : Ast.ty option }
|
||||
|
||||
type state = {
|
||||
aff : (string, bool) Hashtbl.t;
|
||||
(* struct 이름 -> 필드 이름과 타입 *)
|
||||
fields : (string, (string * ty) list) Hashtbl.t;
|
||||
fns : (string, fninfo) Hashtbl.t;
|
||||
meths : (string, (string * fninfo) list) Hashtbl.t;
|
||||
mutable scopes : binding list list;
|
||||
@@ -41,6 +46,10 @@ type state = {
|
||||
mutable depth : int;
|
||||
(* 클로저 프레임: (프레임 깊이, 잡아온 바깥 바인딩). 중첩 클로저를 위해 스택 *)
|
||||
mutable frames : (int * binding list ref) list;
|
||||
(* 클로저 인자를 걸을 때 기대 파라미터 타입. move 검사는 타입 검사와 별도
|
||||
순회라 타입을 모르는데, 클로저 파라미터의 affinity를 알아야 빌림 여부를
|
||||
판정할 수 있다. 호출 대상의 시그니처에서 읽어와 여기 잠깐 둔다. *)
|
||||
mutable cl_expect : fn_param_ty list option;
|
||||
mutable errors : error list;
|
||||
}
|
||||
|
||||
@@ -74,7 +83,10 @@ let derive_affinity st (items : item list) =
|
||||
(fun it ->
|
||||
match it with
|
||||
| I_capability { name; _ } -> Hashtbl.replace st.aff name true
|
||||
| I_struct { name; _ } -> Hashtbl.replace st.aff name false
|
||||
| I_struct { name; fields; _ } ->
|
||||
Hashtbl.replace st.aff name false;
|
||||
Hashtbl.replace st.fields name
|
||||
(List.map (fun f -> (f.f_name, f.f_ty)) fields)
|
||||
| I_enum { name; _ } -> Hashtbl.replace st.aff name false
|
||||
| _ -> ())
|
||||
items;
|
||||
@@ -122,7 +134,7 @@ let derive_affinity st (items : item list) =
|
||||
let push st = st.scopes <- [] :: st.scopes
|
||||
let pop st = match st.scopes with _ :: r -> st.scopes <- r | [] -> ()
|
||||
|
||||
let add st name ~affine ~use ~mut_ =
|
||||
let add st name ~affine ~use ~mut_ ?ty () =
|
||||
st.next_id <- st.next_id + 1;
|
||||
let b =
|
||||
{
|
||||
@@ -132,6 +144,7 @@ let add st name ~affine ~use ~mut_ =
|
||||
b_use = use;
|
||||
b_mut = mut_;
|
||||
b_depth = st.depth;
|
||||
b_ty = ty;
|
||||
}
|
||||
in
|
||||
(match st.scopes with
|
||||
@@ -184,9 +197,29 @@ let merge st snaps =
|
||||
|
||||
type ctx = Borrow | Move of string (* 어디로 옮겨가는지 — 진단에 쓴다 *)
|
||||
|
||||
(* 필드의 선언된 타입. 바인딩의 타입을 알아야 찾을 수 있다 — 모르면 None이고,
|
||||
그때는 객체가 affine인지로 보수적으로 판정한다. *)
|
||||
let field_ty st (obj : expr) (name : string) : ty option =
|
||||
match obj with
|
||||
| E_ident (n, _) -> (
|
||||
match find st n with
|
||||
| Some b -> (
|
||||
match b.b_ty with
|
||||
| Some (T_named { name = tn; _ }) -> (
|
||||
match Hashtbl.find_opt st.fields tn with
|
||||
| Some fs -> List.assoc_opt name fs
|
||||
| None -> None)
|
||||
| _ -> None)
|
||||
| None -> None)
|
||||
| _ -> None
|
||||
|
||||
let rec walk st (ctx : ctx) (e : expr) : vinfo =
|
||||
match e with
|
||||
| E_lit _ -> v_copy
|
||||
(* crash는 돌아오지 않지만 메시지 식은 계산된다. 메시지는 빌려 쓴다 *)
|
||||
| E_crash { msg; _ } ->
|
||||
ignore (walk st Borrow msg);
|
||||
v_copy
|
||||
| E_ident (n, pos) -> (
|
||||
match find st n with
|
||||
| None -> v_copy
|
||||
@@ -264,7 +297,7 @@ let rec walk st (ctx : ctx) (e : expr) : vinfo =
|
||||
ignore (walk st Borrow (E_ident (parent, Token.{ line = 0; col = 0 })));
|
||||
push st;
|
||||
(* 자식 TaskScope는 second-class다 — 블록 밖으로 나갈 수 없다 *)
|
||||
ignore (add st name ~affine:true ~use:true ~mut_:false);
|
||||
ignore (add st name ~affine:true ~use:true ~mut_:false ());
|
||||
let r = walk_block st ctx body in
|
||||
pop st;
|
||||
r
|
||||
@@ -274,11 +307,26 @@ let rec walk st (ctx : ctx) (e : expr) : vinfo =
|
||||
pop st;
|
||||
r
|
||||
| E_call { callee; args; pos } -> walk_call st callee args pos
|
||||
| E_field { obj; _ } ->
|
||||
(* v0에 부분 move는 없다. 필드 접근은 빌림이고 결과도 빌린 값이다. *)
|
||||
| E_field { obj; name; pos } ->
|
||||
(* v0에 부분 move는 없다. 필드 접근은 빌림이고 결과도 빌린 값이다.
|
||||
그러므로 Move 자리에 놓으면 오류다 — 이 검사가 없으면 capability를
|
||||
struct에 넣고 필드를 두 번 읽어 복제할 수 있다 (보안 정리 ii). *)
|
||||
let o = walk st Borrow obj in
|
||||
if o.v_affine || o.v_use then { v_affine = true; v_use = true }
|
||||
else v_copy
|
||||
let fa =
|
||||
match field_ty st obj name with
|
||||
| Some t -> ty_affine st t
|
||||
| None -> o.v_affine || o.v_use
|
||||
in
|
||||
(match ctx with
|
||||
| Borrow -> ()
|
||||
| Move where ->
|
||||
if fa then
|
||||
err st pos
|
||||
(Printf.sprintf
|
||||
"%s 필드는 affine이라 %s 없습니다 (v0에는 부분 move가 없습니다 — 꺼내려면 열거형으로 \
|
||||
감싸십시오)"
|
||||
name where));
|
||||
if fa then { v_affine = true; v_use = true } else v_copy
|
||||
| E_inst { callee; _ } -> walk st Borrow callee
|
||||
| E_try { inner; _ } -> walk st ctx inner
|
||||
| E_unary { operand; _ } -> walk st Borrow operand
|
||||
@@ -291,7 +339,7 @@ and bind_pattern st (info : vinfo) p =
|
||||
match p with
|
||||
| P_wild _ | P_lit _ -> ()
|
||||
| P_bind (n, _) ->
|
||||
ignore (add st n ~affine:info.v_affine ~use:info.v_use ~mut_:false)
|
||||
ignore (add st n ~affine:info.v_affine ~use:info.v_use ~mut_:false ())
|
||||
| P_ctor { args; _ } -> List.iter (bind_pattern st info) args
|
||||
|
||||
and walk_block st ctx (b : block) : vinfo =
|
||||
@@ -310,7 +358,7 @@ and walk_stmt st = function
|
||||
let rec bind p =
|
||||
match p with
|
||||
| P_bind (n, _) ->
|
||||
ignore (add st n ~affine:info.v_affine ~use:info.v_use ~mut_)
|
||||
ignore (add st n ~affine:info.v_affine ~use:info.v_use ~mut_ ())
|
||||
| P_ctor { args; _ } -> List.iter bind args
|
||||
| _ -> ()
|
||||
in
|
||||
@@ -330,10 +378,24 @@ and walk_closure st (c : closure) : vinfo =
|
||||
let acc = ref [] in
|
||||
st.frames <- (st.depth, acc) :: st.frames;
|
||||
push st;
|
||||
List.iter
|
||||
(fun (n, ann) ->
|
||||
let affine = match ann with Some t -> ty_affine st t | None -> false in
|
||||
ignore (add st n ~affine ~use:false ~mut_:false))
|
||||
let expect = st.cl_expect in
|
||||
st.cl_expect <- None;
|
||||
List.iteri
|
||||
(fun i (p : cl_param) ->
|
||||
let affine =
|
||||
match p.cp_ty with
|
||||
| Some t -> ty_affine st t
|
||||
| None -> (
|
||||
(* 표기가 없으면 호출 대상의 시그니처에서 읽어온다 *)
|
||||
match Option.bind expect (fun ps -> List.nth_opt ps i) with
|
||||
| Some pt -> ty_affine st pt.pt_ty
|
||||
| None -> false)
|
||||
in
|
||||
(* 함수 파라미터와 같은 규칙 — 무표기는 빌림이다. 전에는 클로저
|
||||
파라미터를 무조건 소유로 봤고, 그래서 고차 경계에서 소유권 검사가
|
||||
뚫렸다 (dogfoods/FINDINGS D5). *)
|
||||
let use_ = affine && not p.cp_own in
|
||||
ignore (add st p.cp_name ~affine ~use:use_ ~mut_:false ?ty:p.cp_ty ()))
|
||||
c.cl_params;
|
||||
ignore (walk_block st (Move "반환할 수") c.cl_body);
|
||||
pop st;
|
||||
@@ -368,6 +430,9 @@ and walk_call st callee args pos =
|
||||
let own = match p with Some p -> p.p_own | None -> false in
|
||||
let pty = Option.map (fun p -> p.p_ty) p in
|
||||
let ctx = if own then Move "다른 함수에 넘길 수" else Borrow in
|
||||
(match (a, pty) with
|
||||
| E_closure _, Some (T_fn { params; _ }) -> st.cl_expect <- Some params
|
||||
| _ -> st.cl_expect <- None);
|
||||
let got = walk st ctx a in
|
||||
(* callable affinity: affine 클로저를 fn 자리에 넘길 수 없다 *)
|
||||
match pty with
|
||||
@@ -421,7 +486,7 @@ let check_fn st (d : fn_decl) =
|
||||
let affine = ty_affine st p.p_ty in
|
||||
(* 무표기 = 빌림. own만이 소유 이전이다. *)
|
||||
let use_ = affine && not p.p_own in
|
||||
ignore (add st p.p_name ~affine ~use:use_ ~mut_:p.p_mut))
|
||||
ignore (add st p.p_name ~affine ~use:use_ ~mut_:p.p_mut ~ty:p.p_ty ()))
|
||||
d.fn_params;
|
||||
let r = walk_block st (Move "반환할 수") body in
|
||||
(match d.fn_ret with
|
||||
@@ -437,6 +502,7 @@ let check ?(imports : item list = []) (m : modul) : error list =
|
||||
let st =
|
||||
{
|
||||
aff = Hashtbl.create 16;
|
||||
fields = Hashtbl.create 16;
|
||||
fns = Hashtbl.create 16;
|
||||
meths = Hashtbl.create 16;
|
||||
scopes = [];
|
||||
@@ -444,6 +510,7 @@ let check ?(imports : item list = []) (m : modul) : error list =
|
||||
next_id = 0;
|
||||
depth = 0;
|
||||
frames = [];
|
||||
cl_expect = None;
|
||||
errors = [];
|
||||
}
|
||||
in
|
||||
@@ -463,7 +530,16 @@ let check ?(imports : item list = []) (m : modul) : error list =
|
||||
| _ -> ())
|
||||
(imports @ m.items);
|
||||
List.iter
|
||||
(fun it -> match it with I_fn { decl; _ } -> check_fn st decl | _ -> ())
|
||||
(fun it ->
|
||||
match it with
|
||||
| I_fn { decl; _ } -> check_fn st decl
|
||||
(* 테스트도 같은 검사를 받는다 *)
|
||||
| I_test { body; _ } ->
|
||||
st.scopes <- [];
|
||||
push st;
|
||||
ignore (walk_block st (Move "반환할 수") body);
|
||||
pop st
|
||||
| _ -> ())
|
||||
m.items;
|
||||
List.sort
|
||||
(fun a b ->
|
||||
|
||||
+28
-14
@@ -145,7 +145,8 @@ and parse_fn_ty st affine p =
|
||||
if kind st = Token.RParen then []
|
||||
else
|
||||
let rec loop acc =
|
||||
let t = parse_ty st in
|
||||
let own = accept st Token.Kw_own in
|
||||
let t = { pt_own = own; pt_ty = parse_ty st } in
|
||||
if accept st Token.Comma then
|
||||
if kind st = Token.RParen then List.rev (t :: acc) else loop (t :: acc)
|
||||
else List.rev (t :: acc)
|
||||
@@ -398,6 +399,12 @@ and parse_primary st =
|
||||
| Token.Kw_fn -> parse_closure st
|
||||
| Token.Kw_if -> parse_if st
|
||||
| Token.Kw_match -> parse_match st
|
||||
| Token.Kw_crash ->
|
||||
adv st;
|
||||
expect st Token.LParen "(";
|
||||
let msg = with_struct_ok st (fun () -> parse_expr st) in
|
||||
expect_close st Token.RParen ")";
|
||||
E_crash { msg; pos = p }
|
||||
| Token.Kw_scope ->
|
||||
adv st;
|
||||
let name = ident st "새 scope 이름" in
|
||||
@@ -443,12 +450,14 @@ and parse_closure st =
|
||||
if kind st = Token.RParen then []
|
||||
else
|
||||
let rec loop acc =
|
||||
let own = accept st Token.Kw_own in
|
||||
let n = ident st "파라미터 이름" in
|
||||
let t = if accept st Token.Colon then Some (parse_ty st) else None in
|
||||
let cp = { cp_own = own; cp_name = n; cp_ty = t } in
|
||||
if accept st Token.Comma then
|
||||
if kind st = Token.RParen then List.rev ((n, t) :: acc)
|
||||
else loop ((n, t) :: acc)
|
||||
else List.rev ((n, t) :: acc)
|
||||
if kind st = Token.RParen then List.rev (cp :: acc)
|
||||
else loop (cp :: acc)
|
||||
else List.rev (cp :: acc)
|
||||
in
|
||||
loop []
|
||||
in
|
||||
@@ -564,16 +573,11 @@ and parse_stmt st =
|
||||
S_return { value; pos = p }
|
||||
| _ ->
|
||||
let e = parse_expr st in
|
||||
if accept st Token.Eq then begin
|
||||
let rec is_place = function
|
||||
| E_ident _ -> true
|
||||
| E_field { obj; _ } -> is_place obj
|
||||
| _ -> false
|
||||
in
|
||||
if not (is_place e) then err st "대입 왼쪽에는 변수나 필드만 올 수 있습니다";
|
||||
let value = parse_expr st in
|
||||
S_assign { place = e; value; pos = p }
|
||||
end
|
||||
if accept st Token.Eq then
|
||||
(* 대입 왼쪽에 무엇이 올 수 있는지는 구문이 아니라 이름 해소가
|
||||
판정한다. 구문으로 가르면 ident 하나로 대입과 식이 갈리지 않아
|
||||
문법이 LL(1)이 아니게 된다 (grammar.ebnf의 expr_stmt). *)
|
||||
S_assign { place = e; value = parse_expr st; pos = p }
|
||||
else S_expr e
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
@@ -764,6 +768,16 @@ let parse_item st =
|
||||
adv st;
|
||||
let name = ident st "재수출할 이름" in
|
||||
I_reexport { name; pos = p }
|
||||
| Token.Kw_test ->
|
||||
adv st;
|
||||
let name =
|
||||
match kind st with
|
||||
| Token.Str s ->
|
||||
adv st;
|
||||
s
|
||||
| _ -> err_expect st "테스트 이름 (문자열)"
|
||||
in
|
||||
I_test { name; body = parse_block st; pos = p }
|
||||
| _ -> (
|
||||
let pub = accept st Token.Kw_pub in
|
||||
match kind st with
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
(* 문법 파일이 직접 읽는 인식기.
|
||||
|
||||
손으로 쓴 파서(lib/parser.ml)와 같은 토큰 열을 받아 같은 판정을 내야 한다.
|
||||
갈리면 둘 중 하나가 틀린 것이고, 그 순간 테스트가 깨진다. 이것이 설명서와
|
||||
구현이 어긋나지 않게 하는 기계적 장치다.
|
||||
|
||||
AST를 만들지 않는다. 받아들이는가만 답한다. 그래서 손 파서의 진단은
|
||||
그대로 남는다 — 대조는 판정만 한다.
|
||||
|
||||
문법이 LL(1)임이 이미 검증되었으므로 여기서 선택은 결정적이다.
|
||||
다음 토큰이 어느 대안의 FIRST에 있는지만 보면 되고 되돌아가지 않는다. *)
|
||||
|
||||
module SS = Set.Make (String)
|
||||
|
||||
type error = { pos : Token.pos; expected : string; got : string }
|
||||
|
||||
(* 토큰을 문법의 단말 이름으로 옮긴다. 이 대응이 문법과 렉서를 잇는 유일한
|
||||
지점이다 — 여기가 틀리면 대조 전체가 무의미하다. *)
|
||||
let terminal_of (k : Token.kind) =
|
||||
match k with
|
||||
| Token.Ident _ -> "ident"
|
||||
| Token.Int _ -> "int_lit"
|
||||
| Token.Str _ -> "string_lit"
|
||||
| Token.Newline -> "NEWLINE"
|
||||
| Token.Eof -> "<eof>"
|
||||
| k -> Token.show_kind k
|
||||
|
||||
type state = {
|
||||
toks : Token.t array;
|
||||
mutable i : int;
|
||||
rules : (string, Ebnf.rule) Hashtbl.t;
|
||||
a : Ebnf.analysis;
|
||||
tokens : SS.t;
|
||||
mutable err : error option;
|
||||
}
|
||||
|
||||
exception Fail
|
||||
|
||||
let cur st = st.toks.(st.i)
|
||||
let term st = terminal_of (cur st).Token.kind
|
||||
|
||||
let fail st expected =
|
||||
(* 가장 멀리 간 실패를 남긴다. 그 자리가 사람이 볼 자리다 *)
|
||||
let keep =
|
||||
match st.err with
|
||||
| None -> true
|
||||
| Some e ->
|
||||
(e.pos.Token.line, e.pos.Token.col)
|
||||
<= ((cur st).Token.pos.Token.line, (cur st).Token.pos.Token.col)
|
||||
in
|
||||
if keep then
|
||||
st.err <- Some { pos = (cur st).Token.pos; expected; got = term st };
|
||||
raise Fail
|
||||
|
||||
let advance st = if st.i < Array.length st.toks - 1 then st.i <- st.i + 1
|
||||
let is_tok st n = SS.mem n st.tokens || not (Hashtbl.mem st.rules n)
|
||||
|
||||
(* 이 식이 지금 토큰으로 시작할 수 있는가 *)
|
||||
let starts st e = SS.mem (term st) (Ebnf.first_expr st.a e)
|
||||
|
||||
let rec run st (e : Ebnf.expr) =
|
||||
match e with
|
||||
| Ebnf.Term s -> if term st = s then advance st else fail st ("\"" ^ s ^ "\"")
|
||||
| Ebnf.Ref n ->
|
||||
if is_tok st n then if term st = n then advance st else fail st n
|
||||
else run st (Hashtbl.find st.rules n).Ebnf.body
|
||||
| Ebnf.RefArg (n, x) -> run st (Ebnf.Ref (Ebnf.mangle n x))
|
||||
| Ebnf.Seq xs -> List.iter (run st) xs
|
||||
| Ebnf.Alt xs -> (
|
||||
match List.find_opt (starts st) xs with
|
||||
| Some x -> run st x
|
||||
| None -> (
|
||||
(* 비어도 되는 대안이 있으면 그것을 고른다 *)
|
||||
match List.find_opt (Ebnf.nullable_expr st.a) xs with
|
||||
| Some x -> run st x
|
||||
| None ->
|
||||
fail st
|
||||
(String.concat " 또는 " (SS.elements (Ebnf.first_expr st.a e)))))
|
||||
(* 선택과 반복은 최대한 먹는다 (문법 표기 규약의 greedy 규칙) *)
|
||||
| Ebnf.Opt x -> if starts st x then run st x
|
||||
| Ebnf.Rep x ->
|
||||
while starts st x do
|
||||
run st x
|
||||
done
|
||||
| Ebnf.Except (x, _) -> run st x
|
||||
|
||||
let check ?(tokens = []) ?(start = "module") (g : Ebnf.t) (toks : Token.t list)
|
||||
: (unit, error) result =
|
||||
let g = Ebnf.expand g in
|
||||
let rules = Hashtbl.create 128 in
|
||||
List.iter (fun (r : Ebnf.rule) -> Hashtbl.replace rules r.name r) g;
|
||||
let st =
|
||||
{
|
||||
toks = Array.of_list toks;
|
||||
i = 0;
|
||||
rules;
|
||||
a = Ebnf.analyze ~tokens g;
|
||||
tokens = SS.of_list tokens;
|
||||
err = None;
|
||||
}
|
||||
in
|
||||
match run st (Ebnf.Ref start) with
|
||||
| () ->
|
||||
if term st = "<eof>" then Ok ()
|
||||
else (
|
||||
(match st.err with
|
||||
| Some _ -> ()
|
||||
| None ->
|
||||
st.err <-
|
||||
Some
|
||||
{ pos = (cur st).Token.pos; expected = "파일 끝"; got = term st });
|
||||
Error (Option.get st.err))
|
||||
| exception Fail -> Error (Option.get st.err)
|
||||
+12
-6
@@ -104,7 +104,7 @@ let rec resolve_ty st = function
|
||||
then external_ref st name pos;
|
||||
List.iter (resolve_targ st pos) args
|
||||
| T_fn { params; eff; ret; pos; _ } ->
|
||||
List.iter (resolve_ty st) params;
|
||||
List.iter (fun (p : fn_param_ty) -> resolve_ty st p.pt_ty) params;
|
||||
(match eff with None -> () | Some a -> resolve_eff_atom st (a, pos));
|
||||
Option.iter (resolve_ty st) ret
|
||||
|
||||
@@ -154,6 +154,7 @@ let rec resolve_pattern st seen = function
|
||||
|
||||
let rec resolve_expr st = function
|
||||
| E_lit _ -> ()
|
||||
| E_crash { msg; _ } -> resolve_expr st msg
|
||||
| E_ident (n, pos) ->
|
||||
if lookup_local st n = None then
|
||||
if Hashtbl.mem st.items n then ()
|
||||
@@ -169,9 +170,9 @@ let rec resolve_expr st = function
|
||||
| E_closure c ->
|
||||
push st;
|
||||
List.iter
|
||||
(fun (n, t) ->
|
||||
Option.iter (resolve_ty st) t;
|
||||
bind st c.cl_pos n false)
|
||||
(fun (p : cl_param) ->
|
||||
Option.iter (resolve_ty st) p.cp_ty;
|
||||
bind st c.cl_pos p.cp_name false)
|
||||
c.cl_params;
|
||||
(match c.cl_eff with
|
||||
| None -> ()
|
||||
@@ -261,7 +262,7 @@ and resolve_stmt st = function
|
||||
| _ -> None
|
||||
in
|
||||
match root place with
|
||||
| None -> ()
|
||||
| None -> error st pos "대입 왼쪽에는 변수나 필드만 올 수 있습니다"
|
||||
| Some n -> (
|
||||
match lookup_local st n with
|
||||
| Some true -> ()
|
||||
@@ -350,7 +351,7 @@ let resolve (m : modul) : info * error list =
|
||||
variants
|
||||
| I_capability { name; pos; _ } -> declare st pos name K_type
|
||||
| I_const { name; pos; _ } -> declare st pos name K_const
|
||||
| I_reexport _ -> ())
|
||||
| I_test _ | I_reexport _ -> ())
|
||||
m.items;
|
||||
(* 2차: 본문을 훑는다. *)
|
||||
List.iter
|
||||
@@ -378,6 +379,11 @@ let resolve (m : modul) : info * error list =
|
||||
resolve_ty st ty;
|
||||
push st;
|
||||
resolve_expr st value;
|
||||
pop st
|
||||
(* 테스트는 파라미터 없는 본문이다. 나머지는 함수와 같다 *)
|
||||
| I_test { body; _ } ->
|
||||
push st;
|
||||
resolve_block st body;
|
||||
pop st)
|
||||
m.items;
|
||||
(* 미사용 import는 오류다. 취향 문제가 아니라 invalidation 표면 문제다 —
|
||||
|
||||
+33
-9
@@ -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))
|
||||
|
||||
@@ -28,6 +28,8 @@ type kind =
|
||||
| Kw_else
|
||||
| Kw_match
|
||||
| Kw_scope
|
||||
| Kw_crash
|
||||
| Kw_test
|
||||
| Kw_true
|
||||
| Kw_false
|
||||
(* 구두점 *)
|
||||
@@ -87,6 +89,8 @@ let keyword = function
|
||||
| "else" -> Some Kw_else
|
||||
| "match" -> Some Kw_match
|
||||
| "scope" -> Some Kw_scope
|
||||
| "crash" -> Some Kw_crash
|
||||
| "test" -> Some Kw_test
|
||||
| "true" -> Some Kw_true
|
||||
| "false" -> Some Kw_false
|
||||
| _ -> None
|
||||
@@ -115,6 +119,8 @@ let show_kind = function
|
||||
| Kw_else -> "else"
|
||||
| Kw_match -> "match"
|
||||
| Kw_scope -> "scope"
|
||||
| Kw_crash -> "crash"
|
||||
| Kw_test -> "test"
|
||||
| Kw_true -> "true"
|
||||
| Kw_false -> "false"
|
||||
| LParen -> "("
|
||||
@@ -152,6 +158,77 @@ let show_kind = function
|
||||
let show { kind; pos } =
|
||||
Printf.sprintf "%d:%d %s" pos.line pos.col (show_kind kind)
|
||||
|
||||
(* 모든 토큰 종류를 한 번씩 잇는 사슬. 아래 match가 전체 나열이므로 토큰을
|
||||
추가하면 컴파일러가 여기를 지적한다 — 목록이 조용히 낡지 않는다.
|
||||
문법 문서의 어휘 절이 이 나열에서 생성된다. *)
|
||||
let next_kind = function
|
||||
| Ident _ -> Some (Int "")
|
||||
| Int _ -> Some (Str "")
|
||||
| Str _ -> Some Kw_pub
|
||||
| Kw_pub -> Some Kw_fn
|
||||
| Kw_fn -> Some Kw_struct
|
||||
| Kw_struct -> Some Kw_enum
|
||||
| Kw_enum -> Some Kw_capability
|
||||
| Kw_capability -> Some Kw_const
|
||||
| Kw_const -> Some Kw_import
|
||||
| Kw_import -> Some Kw_as
|
||||
| Kw_as -> Some Kw_reexport
|
||||
| Kw_reexport -> Some Kw_let
|
||||
| Kw_let -> Some Kw_mut
|
||||
| Kw_mut -> Some Kw_own
|
||||
| Kw_own -> Some Kw_affine
|
||||
| Kw_affine -> Some Kw_copyable
|
||||
| Kw_copyable -> Some Kw_effects
|
||||
| Kw_effects -> Some Kw_return
|
||||
| Kw_return -> Some Kw_if
|
||||
| Kw_if -> Some Kw_else
|
||||
| Kw_else -> Some Kw_match
|
||||
| Kw_match -> Some Kw_scope
|
||||
| Kw_scope -> Some Kw_crash
|
||||
| Kw_crash -> Some Kw_test
|
||||
| Kw_test -> Some Kw_true
|
||||
| Kw_true -> Some Kw_false
|
||||
| Kw_false -> Some LParen
|
||||
| LParen -> Some RParen
|
||||
| RParen -> Some LBrace
|
||||
| LBrace -> Some RBrace
|
||||
| RBrace -> Some LBracket
|
||||
| LBracket -> Some RBracket
|
||||
| RBracket -> Some Comma
|
||||
| Comma -> Some Colon
|
||||
| Colon -> Some Dot
|
||||
| Dot -> Some Arrow
|
||||
| Arrow -> Some FatArrow
|
||||
| FatArrow -> Some Question
|
||||
| Question -> Some Underscore
|
||||
| Underscore -> Some Eq
|
||||
| Eq -> Some EqEq
|
||||
| EqEq -> Some Bang
|
||||
| Bang -> Some BangEq
|
||||
| BangEq -> Some Lt
|
||||
| Lt -> Some Le
|
||||
| Le -> Some Gt
|
||||
| Gt -> Some Ge
|
||||
| Ge -> Some Plus
|
||||
| Plus -> Some Minus
|
||||
| Minus -> Some Star
|
||||
| Star -> Some Slash
|
||||
| Slash -> Some Percent
|
||||
| Percent -> Some AmpAmp
|
||||
| AmpAmp -> Some PipePipe
|
||||
| PipePipe -> Some Pipe
|
||||
| Pipe -> Some Newline
|
||||
| Newline -> Some Eof
|
||||
| Eof -> None
|
||||
|
||||
let all_kinds =
|
||||
let rec go k acc =
|
||||
match next_kind k with
|
||||
| None -> List.rev (k :: acc)
|
||||
| Some n -> go n (k :: acc)
|
||||
in
|
||||
go (Ident "") []
|
||||
|
||||
(* 줄 끝에서 문 구분자를 삽입할지 결정한다 (grammar.ebnf 어휘 절).
|
||||
값으로 끝날 수 있는 토큰 뒤에서만 삽입하므로, 연산자나 여는 괄호로
|
||||
끝나는 줄은 다음 줄로 이어진다. *)
|
||||
|
||||
+106
-15
@@ -14,7 +14,8 @@ type error = { pos : Token.pos; msg : string }
|
||||
type scheme = {
|
||||
s_gen : string list; (* 타입 파라미터 *)
|
||||
s_eff_gen : string list; (* effect 파라미터 *)
|
||||
s_params : T.t list;
|
||||
(* (own, 타입). 무표기는 빌림 *)
|
||||
s_params : (bool * T.t) list;
|
||||
s_eff : T.eff; (* 이 함수를 부르면 수행되는 effect *)
|
||||
s_ret : T.t;
|
||||
}
|
||||
@@ -43,6 +44,26 @@ type env = {
|
||||
|
||||
let err env pos msg = env.errors <- { pos; msg } :: env.errors
|
||||
|
||||
let expr_pos (e : expr) : Token.pos =
|
||||
match e with
|
||||
| E_lit (_, p)
|
||||
| E_ident (_, p)
|
||||
| E_list (_, p)
|
||||
| E_struct { pos = p; _ }
|
||||
| E_if { pos = p; _ }
|
||||
| E_match { pos = p; _ }
|
||||
| E_scope { pos = p; _ }
|
||||
| E_call { pos = p; _ }
|
||||
| E_field { pos = p; _ }
|
||||
| E_inst { pos = p; _ }
|
||||
| E_try { pos = p; _ }
|
||||
| E_unary { pos = p; _ }
|
||||
| E_binary { pos = p; _ }
|
||||
| E_crash { pos = p; _ } ->
|
||||
p
|
||||
| E_closure c -> c.cl_pos
|
||||
| E_block b -> b.block_pos
|
||||
|
||||
let mismatch env pos expected got what =
|
||||
err env pos
|
||||
(Printf.sprintf "%s: %s이(가) 필요한데 %s입니다" what (T.show expected) (T.show got))
|
||||
@@ -102,7 +123,10 @@ let rec conv env (gen : string list) (t : Ast.ty) : T.t =
|
||||
T.TFn
|
||||
{
|
||||
affine;
|
||||
params = List.map (conv env gen) params;
|
||||
params =
|
||||
List.map
|
||||
(fun (p : fn_param_ty) -> (p.pt_own, conv env gen p.pt_ty))
|
||||
params;
|
||||
eff = (match eff with None -> [] | Some a -> conv_eff_atom a);
|
||||
ret = (match ret with None -> T.TUnit | Some r -> conv env gen r);
|
||||
}
|
||||
@@ -121,7 +145,7 @@ let scheme_of env (d : fn_decl) : scheme =
|
||||
{
|
||||
s_gen = gen;
|
||||
s_eff_gen = egen;
|
||||
s_params = List.map (fun p -> conv env gen p.p_ty) d.fn_params;
|
||||
s_params = List.map (fun p -> (p.p_own, conv env gen p.p_ty)) d.fn_params;
|
||||
s_eff =
|
||||
(match d.fn_eff with None -> [] | Some atoms -> conv_eff_result atoms);
|
||||
s_ret = (match d.fn_ret with None -> T.TUnit | Some r -> conv env gen r);
|
||||
@@ -131,14 +155,14 @@ let scheme_of env (d : fn_decl) : scheme =
|
||||
let instantiate (s : scheme) =
|
||||
let sub = List.map (fun v -> (v, T.fresh ())) s.s_gen in
|
||||
let esub = List.map (fun v -> (v, T.fresh_eff ())) s.s_eff_gen in
|
||||
( List.map (T.subst sub esub) s.s_params,
|
||||
( List.map (fun (o, t) -> (o, T.subst sub esub t)) s.s_params,
|
||||
T.subst_eff esub s.s_eff,
|
||||
T.subst sub esub s.s_ret )
|
||||
|
||||
let instantiate_with (s : scheme) (args : T.t list) =
|
||||
let sub = List.map2 (fun v a -> (v, a)) s.s_gen args in
|
||||
let esub = List.map (fun v -> (v, T.fresh_eff ())) s.s_eff_gen in
|
||||
( List.map (T.subst sub esub) s.s_params,
|
||||
( List.map (fun (o, t) -> (o, T.subst sub esub t)) s.s_params,
|
||||
T.subst_eff esub s.s_eff,
|
||||
T.subst sub esub s.s_ret )
|
||||
|
||||
@@ -164,6 +188,12 @@ let builtin_ctor = function
|
||||
|
||||
let rec infer env (e : expr) : T.t =
|
||||
match e with
|
||||
(* crash는 돌아오지 않는다. 타입은 Never이고 어떤 자리에도 놓인다 *)
|
||||
| E_crash { msg; pos } ->
|
||||
let t = infer env msg in
|
||||
if not (T.unify t T.TString) then
|
||||
mismatch env pos T.TString t "crash의 메시지";
|
||||
T.TNever
|
||||
| E_lit (L_int _, _) -> T.TInt
|
||||
| E_lit (L_str _, _) -> T.TString
|
||||
| E_lit (L_bool _, _) -> T.TBool
|
||||
@@ -358,20 +388,31 @@ and infer_closure env (c : closure) (expected : T.t option) =
|
||||
match Option.map T.resolve expected with
|
||||
| Some (T.TFn { params; ret; _ })
|
||||
when List.length params = List.length c.cl_params ->
|
||||
(List.map Option.some params, Some ret)
|
||||
(List.map (fun (o, t) -> Some (o, t)) params, Some ret)
|
||||
| _ -> (List.map (fun _ -> None) c.cl_params, None)
|
||||
in
|
||||
push env;
|
||||
let param_tys =
|
||||
List.map2
|
||||
(fun (n, ann) exp ->
|
||||
(fun (p : cl_param) exp ->
|
||||
let t =
|
||||
match ann with
|
||||
match p.cp_ty with
|
||||
| Some a -> conv env [] a
|
||||
| None -> ( match exp with Some t -> t | None -> T.TUnknown)
|
||||
| None -> ( match exp with Some (_, t) -> t | None -> T.TUnknown)
|
||||
in
|
||||
bind env n t;
|
||||
t)
|
||||
(* 기대 타입이 소유를 말하는데 리터럴이 안 적었으면 오류다.
|
||||
반대도 오류다 — 빌리는 자리에 소유를 주장하면 빌린 값을 소비한다.
|
||||
방향을 다루려면 부분 타입이 필요하고 우리에겐 없다. *)
|
||||
(match exp with
|
||||
| Some (o, _) when o <> p.cp_own ->
|
||||
err env c.cl_pos
|
||||
(Printf.sprintf "클로저 파라미터 %s의 소유권이 기대와 다릅니다 (기대: %s, 적힌 것: %s)"
|
||||
p.cp_name
|
||||
(if o then "own" else "빌림")
|
||||
(if p.cp_own then "own" else "빌림"))
|
||||
| _ -> ());
|
||||
bind env p.cp_name t;
|
||||
(p.cp_own, t))
|
||||
c.cl_params expected_params
|
||||
in
|
||||
let declared_ret = Option.map (conv env []) c.cl_ret in
|
||||
@@ -420,7 +461,14 @@ and infer_call env callee args pos =
|
||||
| None -> (
|
||||
match builtin_ctor n with
|
||||
| Some (params, ret) ->
|
||||
Some (T.TFn { affine = false; params; eff = []; ret })
|
||||
Some
|
||||
(T.TFn
|
||||
{
|
||||
affine = false;
|
||||
params = List.map (fun t -> (false, t)) params;
|
||||
eff = [];
|
||||
ret;
|
||||
})
|
||||
| None -> (
|
||||
match Hashtbl.find_opt env.fns n with
|
||||
| Some s ->
|
||||
@@ -448,7 +496,7 @@ and infer_call env callee args pos =
|
||||
List.iter (fun a -> ignore (infer env a)) args)
|
||||
else
|
||||
List.iter2
|
||||
(fun p a ->
|
||||
(fun (_, p) a ->
|
||||
let got =
|
||||
match a with
|
||||
| E_closure c -> infer_closure env c (Some p)
|
||||
@@ -481,7 +529,7 @@ and ctor_fn env enum name =
|
||||
let sub = List.map (fun v -> (v, T.fresh ())) gen in
|
||||
let params =
|
||||
match List.assoc_opt name variants with
|
||||
| Some ts -> List.map (T.subst sub []) ts
|
||||
| Some ts -> List.map (fun t -> (false, T.subst sub [] t)) ts
|
||||
| None -> []
|
||||
in
|
||||
T.TFn
|
||||
@@ -640,6 +688,17 @@ and infer_block env (b : block) : T.t =
|
||||
go b.stmts
|
||||
|
||||
and check_stmt env = function
|
||||
(* 꼬리가 아닌 자리의 식은 값을 남기면 안 된다. 남긴 값은 버려지는데,
|
||||
그 값이 Result면 실패가 조용히 사라진다 — 철학 1과 정면으로 부딪힌다.
|
||||
일부러 버리려면 let _ = 로 적는다. 버린다는 사실이 코드에 보여야 한다. *)
|
||||
| S_expr e -> (
|
||||
let t = infer env e in
|
||||
match T.resolve t with
|
||||
| T.TUnit | T.TUnknown | T.TNever -> ()
|
||||
| other ->
|
||||
err env (expr_pos e)
|
||||
(Printf.sprintf "이 식이 남기는 %s이(가) 버려집니다 (일부러 버리려면 let _ = 로 적으십시오)"
|
||||
(T.show other)))
|
||||
| S_let { pat; ty; value; pos; _ } ->
|
||||
let declared = Option.map (conv env []) ty in
|
||||
let got =
|
||||
@@ -662,7 +721,6 @@ and check_stmt env = function
|
||||
let p = infer env place in
|
||||
let v = infer env value in
|
||||
if not (T.unify p v) then mismatch env pos p v "대입"
|
||||
| S_expr e -> ignore (infer env e)
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
(* 모듈 *)
|
||||
@@ -776,6 +834,18 @@ let check ?(imports : item list = []) (m : modul) : error list =
|
||||
errors = [];
|
||||
}
|
||||
in
|
||||
(* 내장 열거형. 이것이 없으면 Option/Result의 패턴이 생성자로 인식되지
|
||||
않고 변수 바인딩이 되어, match 안에서 타입 정보가 통째로 사라진다.
|
||||
사용자 정의가 덮을 수 있도록 먼저 넣는다. *)
|
||||
Hashtbl.replace env.enums "Option"
|
||||
([ "a" ], [ ("Some", [ T.TVar "a" ]); ("None", []) ]);
|
||||
Hashtbl.replace env.enums "Result"
|
||||
([ "a"; "e" ], [ ("Ok", [ T.TVar "a" ]); ("Err", [ T.TVar "e" ]) ]);
|
||||
List.iter
|
||||
(fun (v, e) -> Hashtbl.replace env.ctors v e)
|
||||
[
|
||||
("Some", "Option"); ("None", "Option"); ("Ok", "Result"); ("Err", "Result");
|
||||
];
|
||||
(* 1차: 타입과 생성자 이름부터 (선언 순서에 의존하지 않는다) *)
|
||||
List.iter
|
||||
(fun it ->
|
||||
@@ -823,6 +893,27 @@ let check ?(imports : item list = []) (m : modul) : error list =
|
||||
let want = conv env [] ty in
|
||||
let got = infer env value in
|
||||
if not (T.unify want got) then mismatch env pos want got "상수의 값"
|
||||
(* 테스트는 파라미터 없고 effect 없는 함수와 같다. 검사도 같다 —
|
||||
일반 코드와 다른 규칙을 주면 테스트만 통과하는 코드가 생긴다 *)
|
||||
| I_test { name; body; pos } ->
|
||||
env.locals <- [];
|
||||
push env;
|
||||
env.ret <- T.TUnit;
|
||||
env.performed <- [];
|
||||
env.saw_unknown <- false;
|
||||
let got = infer_block env body in
|
||||
if not (T.unify T.TUnit got) then
|
||||
mismatch env pos T.TUnit got (Printf.sprintf "테스트 \"%s\"의 본문" name);
|
||||
List.iter
|
||||
(fun (a, apos) ->
|
||||
err env apos
|
||||
(Printf.sprintf
|
||||
"테스트는 effect를 수행할 수 없습니다 (%s) — 테스트는 capability를 받지 않습니다"
|
||||
(T.atom_show a)))
|
||||
(List.rev (T.eff_resolve (List.map fst env.performed))
|
||||
|> List.map (fun a -> (a, pos)));
|
||||
env.performed <- [];
|
||||
pop env
|
||||
| _ -> ())
|
||||
m.items;
|
||||
List.sort
|
||||
|
||||
+21
-5
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
+40
-2
@@ -2,14 +2,39 @@
|
||||
//
|
||||
// 본문이 없다. 런타임이 구현하고, 이 파일은 그 계약을 말한다.
|
||||
// 그래서 이 파일은 구현이 아니라 시험대다 — effect 다형성이 실제로 쓸 만한지가
|
||||
// 여기서 결정된다. each와 map이 effect 변수 하나로 표현되지 않으면 규칙이
|
||||
// 틀린 것이고, 그건 v1로 미룰 수 없는 발견이다.
|
||||
// each, map, fold, filter에서 결정된다. 규칙이 틀렸으면 여기서 드러난다.
|
||||
//
|
||||
// e는 파라미터의 effect 슬롯에 홀로 나타난다 (결정 위치). 호출 지점에서
|
||||
// 인자의 시그니처를 읽어 묶인다 — 추론이 아니라 읽기다.
|
||||
|
||||
pub fn len[a](xs: List[a]) -> Int
|
||||
|
||||
pub fn is_empty[a](xs: List[a]) -> Bool
|
||||
|
||||
// 인덱싱 연산자가 언어에 없다. n번째를 꺼내는 일은 이름 있는 함수가 하고,
|
||||
// 없을 수 있다는 사실은 Option이 말한다 — 범위를 벗어나면 예외도 기본값도
|
||||
// 아니고 None이다.
|
||||
pub fn first[a](xs: List[a]) -> Option[a]
|
||||
|
||||
pub fn nth[a](xs: List[a], i: Int) -> Option[a]
|
||||
|
||||
// 번호를 붙인 값. fold에 인덱스를 넣는 대신 이것 하나를 둔다 —
|
||||
// fold_indexed를 만들면 map_indexed, each_indexed가 따라오고 그것이
|
||||
// "한 개념 한 방식"을 깨는 방향이다. enumerate는 기존 각 함수와 조합된다.
|
||||
pub copyable struct Indexed[a] {
|
||||
i: Int,
|
||||
value: a,
|
||||
}
|
||||
|
||||
pub fn enumerate[a](xs: List[a]) -> List[Indexed[a]]
|
||||
|
||||
// 뒤에 하나 붙인 새 리스트. 제자리 수정이 아니다.
|
||||
pub fn push[a](xs: List[a], x: a) -> List[a]
|
||||
|
||||
pub fn concat[a](xs: List[a], ys: List[a]) -> List[a]
|
||||
|
||||
pub fn reverse[a](xs: List[a]) -> List[a]
|
||||
|
||||
pub fn each[a, e: effects](
|
||||
xs: List[a],
|
||||
f: fn(a) effects e,
|
||||
@@ -19,3 +44,16 @@ pub fn map[a, b, e: effects](
|
||||
xs: List[a],
|
||||
f: fn(a) effects e -> b,
|
||||
) effects e -> List[b]
|
||||
|
||||
pub fn filter[a, e: effects](
|
||||
xs: List[a],
|
||||
keep: fn(a) effects e -> Bool,
|
||||
) effects e -> List[a]
|
||||
|
||||
// 누적자는 매 단계 소비되고 새것으로 바뀐다. own이 그 사실을 말한다 —
|
||||
// 빌림으로 적으면 affine 값을 fold로 실어나를 수 없다.
|
||||
pub fn fold[a, acc, e: effects](
|
||||
xs: List[a],
|
||||
own init: acc,
|
||||
f: fn(own acc, a) effects e -> acc,
|
||||
) effects e -> acc
|
||||
|
||||
@@ -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 실패")
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,9 @@
|
||||
(libraries coollang)
|
||||
(deps
|
||||
(glob_files %{workspace_root}/samples/*.cool)
|
||||
(glob_files %{workspace_root}/std/*.cool)))
|
||||
(glob_files %{workspace_root}/std/*.cool)
|
||||
(glob_files %{workspace_root}/docs/*.ebnf)
|
||||
(glob_files_rec %{workspace_root}/dogfoods/**)
|
||||
(glob_files %{workspace_root}/samples/app/*)
|
||||
(glob_files %{workspace_root}/samples/modules/*.cool)
|
||||
(glob_files %{workspace_root}/samples/run/*.cool)))
|
||||
|
||||
+538
-8
@@ -327,8 +327,9 @@ let () =
|
||||
(parse_err "fn f(\n a: Int\n)" = Some "다중 줄 목록에는 후행 콤마가 필요합니다");
|
||||
check "if 머리의 struct 리터럴"
|
||||
(parse_err "fn f() {\n if H { a: 1 } {\n b\n }\n}" <> None);
|
||||
check "대입 왼쪽 검사"
|
||||
(parse_err "fn f() {\n g() = 1\n}" = Some "대입 왼쪽에는 변수나 필드만 올 수 있습니다");
|
||||
(* 대입 왼쪽 제약은 파서가 아니라 이름 해소가 본다. 구문으로 가르면
|
||||
ident 하나로 대입과 식이 갈리지 않아 문법이 LL(1)이 아니게 된다 *)
|
||||
check "대입 왼쪽은 파서가 보지 않는다" (parse_err "fn f() {\n g() = 1\n}" = None);
|
||||
check "한 줄에 문 두 개" (parse_err "fn f() {\n a b\n}" = Some "문 끝에 줄바꿈이 필요합니다")
|
||||
|
||||
(* --- 괄호 안에서는 struct 리터럴이 다시 허용된다 --- *)
|
||||
@@ -392,6 +393,8 @@ let has_err src frag =
|
||||
|
||||
let () =
|
||||
check "중복 정의" (has_err "fn f()\nfn f()" "두 번 정의");
|
||||
(* 파서에서 옮겨온 검사. 구문으로 가르면 문법이 LL(1)이 아니게 된다 *)
|
||||
check "대입 왼쪽" (has_err "fn f() {\n g() = 1\n}" "대입 왼쪽에는 변수나 필드만");
|
||||
check "중복 파라미터" (has_err "fn f(a: Int, a: Int)" "파라미터 a");
|
||||
check "중복 제네릭" (has_err "fn f[a, a]()" "제네릭 파라미터 a");
|
||||
check "선언되지 않은 effect 변수" (has_err "fn f() effects e" "선언되지 않은 effect 변수 e");
|
||||
@@ -1029,9 +1032,11 @@ let console =
|
||||
|
||||
let outputs ?(use = []) src expected =
|
||||
match run_src (imports use ^ console ^ src) with
|
||||
| Ok out -> out = expected
|
||||
| Error e ->
|
||||
Printf.printf " (실행 오류: %s)\n" (Session.string_of_error e);
|
||||
| out, None -> out = expected
|
||||
| out, Some e ->
|
||||
Printf.printf " (실행 오류: %s / 그때까지 출력: %S)\n"
|
||||
(Session.string_of_error e)
|
||||
out;
|
||||
false
|
||||
|
||||
let () =
|
||||
@@ -1110,7 +1115,7 @@ let () =
|
||||
"a\nb\nafter\n");
|
||||
(* 권한은 런타임에서만 온다. main이 선언하지 않으면 존재하지 않는다. *)
|
||||
check "선언하지 않은 capability는 실행 시점에도 없다"
|
||||
(match run_src "pub fn main() { }" with Ok "" -> true | _ -> false);
|
||||
(match run_src "pub fn main() { }" with "", None -> true | _ -> false);
|
||||
check "런타임이 모르는 capability는 거절한다"
|
||||
(match
|
||||
run_src
|
||||
@@ -1119,8 +1124,28 @@ let () =
|
||||
}\n\n\
|
||||
pub fn main(d: Db) effects {Db.read} -> Int { d.read() }"
|
||||
with
|
||||
| Error e -> has_sub e.message "제공하지 않습니다"
|
||||
| Ok _ -> false)
|
||||
| _, Some e -> has_sub e.message "제공하지 않습니다"
|
||||
| _, None -> false)
|
||||
|
||||
(* 실패해도 그때까지의 출력은 남는다. print는 실제로 일어난 effect이고,
|
||||
일어난 일을 안 보여주면 "어디까지 갔나"를 알 수 없다. *)
|
||||
let () =
|
||||
match
|
||||
run_src
|
||||
(imports [ "Int" ] ^ console
|
||||
^ "pub fn risky(a: Int, b: Int) -> Int { a / b }\n\n\
|
||||
pub fn main(c: Console) effects {Console.print} {\n\
|
||||
\ c.print(\"전\")\n\
|
||||
\ c.print(Int.show(risky(10, 0)))\n\
|
||||
\ c.print(\"후\")\n\
|
||||
}")
|
||||
with
|
||||
| out, Some e ->
|
||||
check "0으로 나누면 실행 시점 오류다" (has_sub e.message "0으로 나눌 수 없습니다");
|
||||
check "실패해도 그 전 출력은 남는다" (out = "전\n")
|
||||
| _, None ->
|
||||
check "0으로 나누면 실행 시점 오류다" false;
|
||||
check "실패해도 그 전 출력은 남는다" false
|
||||
|
||||
(* 표준 라이브러리 시그니처가 실제 검사에 쓰이는지.
|
||||
|
||||
@@ -1232,3 +1257,508 @@ let () =
|
||||
in
|
||||
List.exists (fun m -> has_sub m "가져왔지만 쓰지 않습니다") ms
|
||||
&& List.exists (fun m -> has_sub m "String이(가) 필요한데 Int") ms)
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
(* 실제 프로그램 *)
|
||||
(* *)
|
||||
(* samples/app은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴 것이다. *)
|
||||
(* 언어가 쓸 만한지는 이런 코드에서만 드러난다. *)
|
||||
(* ------------------------------------------------------------------ *)
|
||||
|
||||
let () =
|
||||
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
|
||||
match
|
||||
Session.run
|
||||
~args:[ "../samples/app/example.conf" ]
|
||||
st "../samples/app/main.cool"
|
||||
with
|
||||
| _, Some e ->
|
||||
Printf.printf " (실행 오류: %s)\n" (Session.string_of_error e);
|
||||
check "app: 설정 리포트가 돈다" false
|
||||
| out, None ->
|
||||
check "app: 항목과 문제를 센다" (has_sub out "항목 5개, 문제 2개");
|
||||
check "app: 값의 타입을 모양으로 정한다"
|
||||
(has_sub out "threads = 4 (number)"
|
||||
&& has_sub out "verbose = true (flag)"
|
||||
&& has_sub out "name = \"coollang\" (text)");
|
||||
check "app: 문제에 줄 번호가 붙는다"
|
||||
(has_sub out "10행: 이름이 비어 있습니다" && has_sub out "11행: = 가 하나여야 합니다");
|
||||
check "app: 타입 있는 조회" (has_sub out "port = 8080")
|
||||
|
||||
let () =
|
||||
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
|
||||
match Session.run st "../samples/app/main.cool" with
|
||||
| out, None -> check "app: 인자가 없으면 말해준다" (has_sub out "경로가 필요합니다")
|
||||
| _, Some _ -> check "app: 인자가 없으면 말해준다" false
|
||||
|
||||
let () =
|
||||
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
|
||||
match Session.run ~args:[ "/없는/파일.conf" ] st "../samples/app/main.cool" with
|
||||
| out, None -> check "app: 없는 파일을 Err로 돌려준다" (has_sub out "오류: ")
|
||||
| _, Some _ -> check "app: 없는 파일을 Err로 돌려준다" false
|
||||
|
||||
(* std 선언과 런타임 구현이 어긋나면 검사는 통과하고 실행이 죽는다.
|
||||
v0에서 둘은 다른 파일에 있으므로 일치는 테스트가 지킨다 (friction F7). *)
|
||||
let () =
|
||||
let dir = "../std" in
|
||||
let declared =
|
||||
Sys.readdir dir |> Array.to_list
|
||||
|> List.filter (fun f -> Filename.check_suffix f ".cool")
|
||||
|> List.concat_map (fun f ->
|
||||
let m = ref "" in
|
||||
(match Driver.ast (Filename.concat dir f) with
|
||||
| Error _ -> ()
|
||||
| Ok _ -> m := Filename.remove_extension f);
|
||||
match Driver.ast (Filename.concat dir f) with
|
||||
| Error _ -> []
|
||||
| Ok ast ->
|
||||
List.filter_map
|
||||
(function
|
||||
(* 본문이 있으면 coollang으로 구현된 것이다 (std/test.cool의
|
||||
assert). 런타임 구현이 필요한 것은 본문 없는 선언뿐이다 *)
|
||||
| Ast.I_fn { decl; _ } when decl.fn_body = None ->
|
||||
Some (!m ^ "." ^ decl.fn_name)
|
||||
| _ -> None)
|
||||
ast.Ast.items)
|
||||
in
|
||||
check "std에 선언이 있다" (List.length declared > 20);
|
||||
List.iter
|
||||
(fun name ->
|
||||
check
|
||||
(Printf.sprintf "std 선언 %s에 런타임 구현이 있다" name)
|
||||
(List.mem name Interp.implemented))
|
||||
declared;
|
||||
List.iter
|
||||
(fun name ->
|
||||
check
|
||||
(Printf.sprintf "런타임 구현 %s에 std 선언이 있다" name)
|
||||
(List.mem name declared))
|
||||
Interp.implemented
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
(* 문법 파일 *)
|
||||
(* *)
|
||||
(* docs/grammar.ebnf는 이제 문서가 아니라 기계가 읽는 소스다. 여기서 *)
|
||||
(* 검사하는 것 셋: *)
|
||||
(* 1. 기계가 읽을 수 있는가 *)
|
||||
(* 2. 정의되지 않은 이름이 토큰 부류뿐인가 *)
|
||||
(* 3. LL(1)인가 — 문법 첫머리의 주장이 여기서 검증된다 *)
|
||||
(* ------------------------------------------------------------------ *)
|
||||
|
||||
let () =
|
||||
let src =
|
||||
let ic = open_in_bin "../docs/grammar.ebnf" in
|
||||
let n = in_channel_length ic in
|
||||
let s = really_input_string ic n in
|
||||
close_in ic;
|
||||
s
|
||||
in
|
||||
match Ebnf.parse_result src with
|
||||
| Error e ->
|
||||
Printf.printf " (문법 파일 %d행: %s)\n" e.line e.msg;
|
||||
check "grammar.ebnf를 읽을 수 있다" false
|
||||
| Ok g ->
|
||||
check "grammar.ebnf를 읽을 수 있다" true;
|
||||
check "프로덕션이 충분히 있다" (List.length g > 50);
|
||||
(* 어휘 층의 이름만 정의 없이 참조될 수 있다 *)
|
||||
let allowed = [ "NEWLINE"; "char"; "digit"; "letter" ] in
|
||||
let undef = Ebnf.undefined g in
|
||||
List.iter
|
||||
(fun n ->
|
||||
check
|
||||
(Printf.sprintf "grammar.ebnf: %s는 정의되었거나 토큰 부류다" n)
|
||||
(List.mem n allowed))
|
||||
undef;
|
||||
check "grammar.ebnf에 죽은 프로덕션이 없다" (Ebnf.unreachable g ~start:"module" = []);
|
||||
let g = Ebnf.expand g in
|
||||
let tokens = allowed @ [ "ident"; "int_lit"; "string_lit" ] in
|
||||
let real =
|
||||
List.filter
|
||||
(fun (c : Ebnf.conflict) -> not c.c_greedy)
|
||||
(Ebnf.conflicts ~tokens ~greedy:[ "NEWLINE" ] g)
|
||||
in
|
||||
List.iter
|
||||
(fun (c : Ebnf.conflict) ->
|
||||
Printf.printf " (LL(1) 충돌 %s:%d [%s] %s — %s)\n" c.c_rule c.c_line
|
||||
c.c_kind
|
||||
(String.concat " " c.c_tokens)
|
||||
c.c_detail)
|
||||
real;
|
||||
check "grammar.ebnf는 LL(1)이다" (real = [])
|
||||
|
||||
(* 문법 문서의 어휘 절은 코드에서 생성된다. 두 곳에 손으로 적힌 목록은
|
||||
어긋난다 — 이 프로젝트가 그것으로 한 번 데었다. *)
|
||||
let () =
|
||||
let src =
|
||||
let ic = open_in_bin "../docs/grammar.ebnf" in
|
||||
let n = in_channel_length ic in
|
||||
let s = really_input_string ic n in
|
||||
close_in ic;
|
||||
s
|
||||
in
|
||||
let block = Lexical_doc.render () in
|
||||
check "문법 문서의 어휘 절이 token.ml과 일치한다" (has_sub src block);
|
||||
if not (has_sub src block) then begin
|
||||
print_endline " 현재 코드가 만드는 블록:";
|
||||
print_endline block
|
||||
end
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
(* 문법과 파서의 대조 *)
|
||||
(* *)
|
||||
(* 문법 파일에서 직접 읽는 인식기와 손으로 쓴 파서에 같은 토큰 열을 주고 *)
|
||||
(* 판정이 같은지 본다. 갈리면 둘 중 하나가 틀린 것이고 빌드가 깨진다. *)
|
||||
(* 이것이 설명서와 구현이 어긋나지 않게 하는 유일한 장치다 — *)
|
||||
(* "문서를 잘 관리하자"는 이미 실패했다. *)
|
||||
(* ------------------------------------------------------------------ *)
|
||||
|
||||
let () =
|
||||
let read f =
|
||||
let ic = open_in_bin f in
|
||||
let n = in_channel_length ic in
|
||||
let s = really_input_string ic n in
|
||||
close_in ic;
|
||||
s
|
||||
in
|
||||
let g =
|
||||
match Ebnf.parse_result (read "../docs/grammar.ebnf") with
|
||||
| Ok g -> g
|
||||
| Error e -> failwith (Printf.sprintf "문법 %d행: %s" e.line e.msg)
|
||||
in
|
||||
let tokens =
|
||||
[ "ident"; "int_lit"; "string_lit"; "NEWLINE"; "char"; "digit"; "letter" ]
|
||||
in
|
||||
let files =
|
||||
List.concat_map
|
||||
(fun dir ->
|
||||
try
|
||||
Sys.readdir dir |> Array.to_list
|
||||
|> List.filter (fun f -> Filename.check_suffix f ".cool")
|
||||
|> List.sort compare
|
||||
|> List.map (Filename.concat dir)
|
||||
with Sys_error _ -> [])
|
||||
[
|
||||
"../samples";
|
||||
"../samples/modules";
|
||||
"../samples/run";
|
||||
"../samples/app";
|
||||
"../std";
|
||||
]
|
||||
in
|
||||
check "대조할 파일이 있다" (List.length files > 15);
|
||||
List.iter
|
||||
(fun f ->
|
||||
match Lexer.lex_result (read f) with
|
||||
| Error _ -> ()
|
||||
| Ok toks ->
|
||||
let hand = Result.is_ok (Parser.parse_result toks) in
|
||||
let spec = Result.is_ok (Recognize.check ~tokens g toks) in
|
||||
if hand <> spec then
|
||||
Printf.printf " (%s: 파서 %s / 문법 %s)\n" f
|
||||
(if hand then "받음" else "거부")
|
||||
(if spec then "받음" else "거부");
|
||||
check (Filename.basename f ^ ": 문법과 파서의 판정이 같다") (hand = spec))
|
||||
files
|
||||
|
||||
(* 문법이 약속한 것을 파서가 실제로 읽는가.
|
||||
저장소의 .cool 파일만으로는 사람이 쓴 코드만 훑는다 — 아무도 안 밟은
|
||||
구석은 드러나지 않는다. 문법에서 문장을 만들어 파서에 먹인다.
|
||||
커버리지를 같이 재는 이유: 안 밟은 규칙은 시험되지 않은 규칙이다. *)
|
||||
let () =
|
||||
let read f =
|
||||
let ic = open_in_bin f in
|
||||
let n = in_channel_length ic in
|
||||
let s = really_input_string ic n in
|
||||
close_in ic;
|
||||
s
|
||||
in
|
||||
match Ebnf.parse_result (read "../docs/grammar.ebnf") with
|
||||
| Error _ -> ()
|
||||
| Ok g ->
|
||||
let tokens =
|
||||
[
|
||||
"ident"; "int_lit"; "string_lit"; "NEWLINE"; "char"; "digit"; "letter";
|
||||
]
|
||||
in
|
||||
let visited = Hashtbl.create 128 in
|
||||
let bad = ref 0 in
|
||||
let shown = ref 0 in
|
||||
for i = 1 to 2500 do
|
||||
Random.init i;
|
||||
let toks = Ebnf_gen.sentence ~tokens ~visited ~max_depth:18 g in
|
||||
match Parser.parse_result toks with
|
||||
| Ok _ -> ()
|
||||
| Error e ->
|
||||
incr bad;
|
||||
if !shown < 3 then begin
|
||||
incr shown;
|
||||
Printf.printf " (씨앗 %d: 문법은 만들었는데 파서가 거부 — %s)\n" i e.msg
|
||||
end
|
||||
done;
|
||||
check "문법이 만든 문장을 파서가 전부 받는다" (!bad = 0);
|
||||
let lexical =
|
||||
[
|
||||
"ident";
|
||||
"int_lit";
|
||||
"string_lit";
|
||||
"str_char";
|
||||
"escape";
|
||||
"bool_lit";
|
||||
"literal";
|
||||
]
|
||||
in
|
||||
let all =
|
||||
Ebnf.expand g
|
||||
|> List.map (fun (r : Ebnf.rule) -> r.name)
|
||||
|> List.sort_uniq compare
|
||||
|> List.filter (fun r -> not (List.mem r lexical))
|
||||
in
|
||||
let unvisited = List.filter (fun r -> not (Hashtbl.mem visited r)) all in
|
||||
if unvisited <> [] then
|
||||
Printf.printf " (밟지 않은 프로덕션: %s)\n" (String.concat " " unvisited);
|
||||
check "생성이 모든 프로덕션을 밟는다" (unvisited = [])
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
(* panic, Never, test *)
|
||||
(* ------------------------------------------------------------------ *)
|
||||
|
||||
let () =
|
||||
let dir = Filename.concat (Filename.get_temp_dir_name ()) "cool_crashtest" in
|
||||
ignore (Sys.command (Printf.sprintf "mkdir -p %s" (Filename.quote dir)));
|
||||
let f = Filename.concat dir "m.cool" in
|
||||
let run src =
|
||||
write f src;
|
||||
let st = Session.create ~root:dir ~std:"../std" () in
|
||||
Session.test st f
|
||||
in
|
||||
let src =
|
||||
"import \"cool.dev/std/test\" as Test\n\n\
|
||||
pub enum E {\n\
|
||||
\ A,\n\
|
||||
\ B,\n\
|
||||
}\n\n\
|
||||
pub fn name(e: E) -> String {\n\
|
||||
\ match e {\n\
|
||||
\ A => \"a\",\n\
|
||||
\ B => crash(\"B는 아직\"),\n\
|
||||
\ }\n\
|
||||
}\n\n\
|
||||
test \"통과\" {\n\
|
||||
\ Test.assert(name(A) == \"a\")\n\
|
||||
}\n\n\
|
||||
test \"assert 실패\" {\n\
|
||||
\ Test.assert(1 == 2)\n\
|
||||
}\n\n\
|
||||
test \"crash 실패\" {\n\
|
||||
\ Test.assert(name(B) == \"b\")\n\
|
||||
}"
|
||||
in
|
||||
match run src with
|
||||
| Error es ->
|
||||
List.iter
|
||||
(fun (e : Session.error) -> Printf.printf " (%s)\n" e.message)
|
||||
es;
|
||||
check "crash/test 예제가 검사를 통과한다" false
|
||||
| Ok rs ->
|
||||
check "crash/test 예제가 검사를 통과한다" true;
|
||||
check "테스트 셋이 잡힌다" (List.length rs = 3);
|
||||
let failed =
|
||||
List.filter (fun (r : Interp.test_result) -> r.t_failure <> None) rs
|
||||
in
|
||||
check "실패는 둘이다" (List.length failed = 2);
|
||||
(* Never가 match 팔에 서지 못하면 위 예제가 타입 검사를 통과하지 못한다 *)
|
||||
check "assert 실패가 보고된다"
|
||||
(List.exists
|
||||
(fun (r : Interp.test_result) ->
|
||||
r.t_name = "assert 실패"
|
||||
&&
|
||||
match r.t_failure with
|
||||
| Some (_, m) -> has_sub m "assert 실패"
|
||||
| None -> false)
|
||||
rs);
|
||||
check "crash 메시지가 그대로 보고된다"
|
||||
(List.exists
|
||||
(fun (r : Interp.test_result) ->
|
||||
r.t_name = "crash 실패"
|
||||
&&
|
||||
match r.t_failure with
|
||||
| Some (_, m) -> has_sub m "B는 아직"
|
||||
| None -> false)
|
||||
rs);
|
||||
(* 하나가 죽어도 나머지는 돈다 — 격리는 런타임의 일이다 *)
|
||||
check "실패해도 다른 테스트는 돈다"
|
||||
(List.exists
|
||||
(fun (r : Interp.test_result) ->
|
||||
r.t_name = "통과" && r.t_failure = None)
|
||||
rs)
|
||||
|
||||
(* 테스트는 effect를 수행할 수 없다 — capability를 받지 않으므로 증명된다 *)
|
||||
let () =
|
||||
let dir = Filename.concat (Filename.get_temp_dir_name ()) "cool_crashtest" in
|
||||
let f = Filename.concat dir "e.cool" in
|
||||
write f
|
||||
"pub capability Db {\n\
|
||||
\ fn read() effects {Db.read} -> Int\n\
|
||||
}\n\n\
|
||||
pub fn sneaky() effects {Db.read} -> Int\n\n\
|
||||
test \"금지\" {\n\
|
||||
\ let n = sneaky()\n\
|
||||
}";
|
||||
let st = Session.create ~root:dir ~std:"../std" () in
|
||||
Session.load st f;
|
||||
check "테스트는 effect를 수행할 수 없다"
|
||||
(List.exists
|
||||
(fun (e : Session.error) -> has_sub e.message "테스트는 effect를 수행할 수 없습니다")
|
||||
(Session.errors st))
|
||||
|
||||
(* samples/app의 테스트가 실제로 돈다 *)
|
||||
let () =
|
||||
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
|
||||
match Session.test st "../samples/app/config.cool" with
|
||||
| Error es ->
|
||||
List.iter
|
||||
(fun (e : Session.error) -> Printf.printf " (%s)\n" e.message)
|
||||
es;
|
||||
check "samples/app의 테스트가 돈다" false
|
||||
| Ok rs ->
|
||||
check "samples/app의 테스트가 돈다" (List.length rs >= 4);
|
||||
check "samples/app의 테스트가 전부 통과한다"
|
||||
(List.for_all (fun (r : Interp.test_result) -> r.t_failure = None) rs)
|
||||
|
||||
(* crash는 scope 트리를 타고 올라간다. 자식이 crash하면 그 scope는 실패하고,
|
||||
scope 뒤의 코드는 실행되지 않는다.
|
||||
|
||||
v0는 태스크가 하나라 이 규칙이 축약된 형태로만 관측된다. 아래에서 "자식
|
||||
셋"이 안 도는 것은 취소가 아니라 순차 실행의 부산물이다 — 취소는 아직
|
||||
정하지 않았다. 고정하는 것은 전파이지 형제의 운명이 아니다. *)
|
||||
let () =
|
||||
match
|
||||
run_src
|
||||
(console
|
||||
^ "pub fn main(c: Console, root: TaskScope)\n\
|
||||
\ effects {Console.print, TaskScope.spawn} {\n\
|
||||
\ c.print(\"scope 앞\")\n\
|
||||
\ scope work = root {\n\
|
||||
\ work.spawn(fn() { c.print(\"자식 하나\") })\n\
|
||||
\ work.spawn(fn() { crash(\"자식 둘이 죽었다\") })\n\
|
||||
\ }\n\
|
||||
\ c.print(\"scope 뒤\")\n\
|
||||
}")
|
||||
with
|
||||
| out, Some e ->
|
||||
check "자식의 crash가 scope 밖으로 전파된다" (has_sub e.message "자식 둘이 죽었다");
|
||||
check "crash 전의 출력은 남는다" (has_sub out "자식 하나");
|
||||
check "scope 뒤는 실행되지 않는다" (not (has_sub out "scope 뒤"))
|
||||
| _, None ->
|
||||
check "자식의 crash가 scope 밖으로 전파된다" false;
|
||||
check "crash 전의 출력은 남는다" false;
|
||||
check "scope 뒤는 실행되지 않는다" false
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
(* dogfoods — 언어가 실제 문제를 표현하는가 *)
|
||||
(* *)
|
||||
(* samples가 검사기를 시험한다면 여기는 언어를 시험한다. 통과가 회귀 *)
|
||||
(* 테스트가 된다 — 규칙을 바꿨을 때 어느 유스케이스가 깨지는지 보인다. *)
|
||||
(* *)
|
||||
(* 기준이 둘인 이유: check만 보면 공허하다. import가 조용히 해소되지 *)
|
||||
(* 않으면 그 모듈의 이름이 전부 불투명해져 무엇이든 통과한다. *)
|
||||
(* 실제로 첫 시도에서 그 일이 일어났다 (FINDINGS D1). *)
|
||||
(* ------------------------------------------------------------------ *)
|
||||
|
||||
let () =
|
||||
let cases =
|
||||
[
|
||||
("../dogfoods/std-draft/fs.cool", []);
|
||||
( "../dogfoods/atomic-update/naive.cool",
|
||||
[ "../dogfoods/std-draft/fs.cool" ] );
|
||||
( "../dogfoods/atomic-update/careful.cool",
|
||||
[ "../dogfoods/std-draft/fs.cool" ] );
|
||||
]
|
||||
in
|
||||
List.iter
|
||||
(fun (path, deps) ->
|
||||
let st = Session.create ~root:(Filename.dirname path) ~std:"../std" () in
|
||||
Session.load st path;
|
||||
let errs = Session.errors st in
|
||||
List.iter
|
||||
(fun (e : Session.error) ->
|
||||
Printf.printf " (%s)\n" (Session.string_of_error e))
|
||||
errs;
|
||||
check (Filename.basename path ^ ": check 통과") (errs = []);
|
||||
(* 의존 모듈이 실제로 로드됐는가 — 이게 없으면 위 줄이 공허하다.
|
||||
경로 표기가 여러 가지라(../a/../b) 파일 이름으로 맞춘다 *)
|
||||
let loaded =
|
||||
Hashtbl.fold
|
||||
(fun k _ acc -> Filename.basename k :: acc)
|
||||
st.Session.modules []
|
||||
in
|
||||
List.iter
|
||||
(fun d ->
|
||||
check
|
||||
(Filename.basename path ^ ": " ^ Filename.basename d
|
||||
^ "이(가) 실제로 해소된다")
|
||||
(List.mem (Filename.basename d) loaded))
|
||||
deps)
|
||||
cases
|
||||
|
||||
(* D3: 자원을 들고 ?를 쓰면 정리를 건너뛰는데 검사기는 통과시킨다.
|
||||
v0 정책("오용 금지, 누수 허용")이고, with가 닫으려는 자리다.
|
||||
이 테스트는 그 사실을 고정한다 — 나중에 정책이 바뀌면 여기가 깨진다. *)
|
||||
let () =
|
||||
let st = Session.create ~root:"../dogfoods/atomic-update" ~std:"../std" () in
|
||||
Session.load st "../dogfoods/atomic-update/naive.cool";
|
||||
check "누수는 아직 오류가 아니다 (v0 정책)" (Session.errors st = [])
|
||||
|
||||
(* D10 회귀: Option/Result 패턴이 타입을 나른다.
|
||||
전에는 내장 생성자가 typecheck의 ctors에 없어 None이 변수 바인딩이 되고
|
||||
Ok(v)의 v가 TUnknown이었다 — 즉 핵심 오류 처리 수단의 패턴 매칭이 타입을
|
||||
전혀 검사하지 않았다. *)
|
||||
let () =
|
||||
check "Ok(v)의 v가 타입을 갖는다"
|
||||
(type_has
|
||||
"fn f(r: Result[Int, String]) -> String {\n\
|
||||
\ match r {\n\
|
||||
\ Ok(v) => v,\n\
|
||||
\ Err(e) => e,\n\
|
||||
\ }\n\
|
||||
}"
|
||||
"String이(가) 필요한데 Int");
|
||||
check "None은 변수 바인딩이 아니다"
|
||||
(type_has
|
||||
"fn f(o: Option[Int]) -> Int {\n\
|
||||
\ match o {\n\
|
||||
\ None => \"x\",\n\
|
||||
\ Some(v) => v,\n\
|
||||
\ }\n\
|
||||
}"
|
||||
"match 팔의 타입이 서로 다릅니다")
|
||||
|
||||
(* 보안 정리 (ii): safe code에서 capability는 복제되지 않는다.
|
||||
struct 필드를 통한 구멍이 있었다 — 필드 접근이 빌린 값을 돌려주는데
|
||||
Move 자리에서 그것을 검사하는 곳이 없었다. 빌린 wrapper에서도 됐다. *)
|
||||
let () =
|
||||
let src =
|
||||
"capability Pay {\n\
|
||||
\ fn charge(n: Int) effects {Pay.charge}\n\
|
||||
}\n\n\
|
||||
struct Wrapper {\n\
|
||||
\ pay: Pay,\n\
|
||||
\ label: String,\n\
|
||||
}\n\n\
|
||||
fn consume(own p: Pay) effects {Pay.charge}\n\n"
|
||||
in
|
||||
check "capability를 필드로 복제할 수 없다"
|
||||
(move_has
|
||||
(src
|
||||
^ "fn duplicate(w: Wrapper) effects {Pay.charge} {\n\
|
||||
\ consume(w.pay)\n\
|
||||
\ consume(w.pay)\n\
|
||||
}")
|
||||
"부분 move가 없습니다");
|
||||
check "소유한 struct에서도 필드를 꺼낼 수 없다"
|
||||
(move_has
|
||||
(src
|
||||
^ "fn take(own w: Wrapper) effects {Pay.charge} {\n consume(w.pay)\n}")
|
||||
"부분 move가 없습니다");
|
||||
check "copyable 필드는 막지 않는다"
|
||||
(move_errs (src ^ "fn label_of(w: Wrapper) -> String {\n w.label\n}") = [])
|
||||
|
||||
@@ -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