Compare commits
30
Commits
5e034c0891
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bfdcaa0d6 | ||
|
|
28001d9c1d | ||
|
|
01d090b065 | ||
|
|
450e96d326 | ||
|
|
088e1992a5 | ||
|
|
d85816abae | ||
|
|
410363b230 | ||
|
|
2307bafda2 | ||
|
|
5dc214baee | ||
|
|
df775ac4a6 | ||
|
|
78ef07d2ee | ||
|
|
8ff35c5d9b | ||
|
|
bb19a39e04 | ||
|
|
61b1920909 | ||
|
|
bde940eda3 | ||
|
|
6c0d08b6b0 | ||
|
|
5e70712578 | ||
|
|
a9f6527825 | ||
|
|
7be05be77f | ||
|
|
5831af7760 | ||
|
|
5ac899df68 | ||
|
|
91f3840d19 | ||
|
|
5593b54772 | ||
|
|
1c4f46e5e5 | ||
|
|
120ff361cf | ||
|
|
c4662ab627 | ||
|
|
801a7b330b | ||
|
|
5218bc59a7 | ||
|
|
2e67b74376 | ||
|
|
79e4ed4190 |
@@ -0,0 +1,133 @@
|
|||||||
|
# coollang
|
||||||
|
|
||||||
|
되돌리기 비싼 결정부터 확정하는 프로그래밍 언어. 현재 v0 — 설계 검증판.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ coolc check samples/run/hello.cool
|
||||||
|
$ coolc run samples/run/hello.cool
|
||||||
|
area = 12
|
||||||
|
area = 9
|
||||||
|
area = 3
|
||||||
|
```
|
||||||
|
|
||||||
|
## 무엇을 위한 언어인가
|
||||||
|
|
||||||
|
세 가지 목표가 나머지 모든 결정을 지배한다.
|
||||||
|
|
||||||
|
1. **오류를 더 빨리 잡는다** — null 없음, Option/Result, 소진적 match,
|
||||||
|
확정 초기화, 선언되지 않은 effect는 컴파일 오류.
|
||||||
|
2. **검증이 더 빠르다** — fast path / slow path 분리, 전역 추론 없음,
|
||||||
|
복잡한 trait solver 없음, 임의 매크로 없음, 인터페이스 해시 기반 무효화.
|
||||||
|
3. **피해 범위가 좁다** — 명시적 capability, ambient authority 없음,
|
||||||
|
affine 소유권.
|
||||||
|
|
||||||
|
설계의 전문은 [`docs/thesis.md`](docs/thesis.md), 문법은
|
||||||
|
[`docs/grammar.ebnf`](docs/grammar.ebnf)에 있다.
|
||||||
|
|
||||||
|
## 지금 되는 것
|
||||||
|
|
||||||
|
`lex → parse → 이름 해소 → 타입 검사 → effect/capability 검사 →
|
||||||
|
move/affinity 검사 → 소진성 검사 → interface 해시 → typed IR → 인터프리터`
|
||||||
|
|
||||||
|
여섯 종류의 오류를 한국어 진단으로 보고한다: 문법, 이름, 타입, effect,
|
||||||
|
capability, 소유권. 여기에 match 소진성과 두 가지 lint(미사용 import,
|
||||||
|
effect 과잉 선언)가 더해진다.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ coolc check samples/12_stdlib_effects.cool
|
||||||
|
samples/12_stdlib_effects.cool:20:14: 선언되지 않은 effect Console.print
|
||||||
|
(leaks_effect의 effects 절은 {}입니다)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 증분 검사
|
||||||
|
|
||||||
|
이 프로젝트의 중심 주장이다. 10만 줄 / 200 모듈에서 측정 (`bench/bench.ml`):
|
||||||
|
|
||||||
|
| | 시간 | 재검사한 모듈 |
|
||||||
|
|---|---|---|
|
||||||
|
| 전체 검사 (cold) | 253ms | 200 |
|
||||||
|
| 함수 **본문**만 수정 | 0.8ms | **1** |
|
||||||
|
| 함수 **시그니처** 수정 | 2.5ms | **2** |
|
||||||
|
|
||||||
|
핵심은 시간이 아니라 범위다. 본문 수정이 downstream을 한 칸도 건드리지
|
||||||
|
않는 것, 그리고 시그니처 수정이 사슬 끝까지 가지 않고 해시가 변한 곳에서
|
||||||
|
멈추는 것 — 이 둘이 아키텍처의 주장 전부다.
|
||||||
|
|
||||||
|
시그니처 수정 시나리오에서는 두 모듈 건너의 `match`가 실제로 깨진다:
|
||||||
|
|
||||||
|
```
|
||||||
|
m100.cool:15:3: match가 모든 경우를 덮지 않습니다 (빠진 경우: Tri(_))
|
||||||
|
m101.cool:21:3: match가 모든 경우를 덮지 않습니다 (빠진 경우: Up.Tri(_))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 실제로 써본 결과
|
||||||
|
|
||||||
|
`samples/app`은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴
|
||||||
|
프로그램이다 — 설정 파서 + 리포트 도구, 2모듈 232줄.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ coolc run samples/app/main.cool samples/app/example.conf
|
||||||
|
설정
|
||||||
|
name = "coollang" (text)
|
||||||
|
threads = 4 (number)
|
||||||
|
...
|
||||||
|
항목 5개, 문제 2개
|
||||||
|
|
||||||
|
문제
|
||||||
|
10행: 이름이 비어 있습니다
|
||||||
|
11행: = 가 하나여야 합니다: broken = a = b
|
||||||
|
```
|
||||||
|
|
||||||
|
쓰면서 걸린 것들을 [`docs/friction.md`](docs/friction.md)에 남겼다.
|
||||||
|
요약하면: **되돌리기 비싼 결정은 하나도 후회되지 않았고, 불편은 전부
|
||||||
|
되돌리기 싼 것들이었다.**
|
||||||
|
|
||||||
|
## 빌드
|
||||||
|
|
||||||
|
OCaml 5.x와 dune이 필요하다.
|
||||||
|
|
||||||
|
```
|
||||||
|
opam install dune
|
||||||
|
dune build
|
||||||
|
dune test # 175개 검사
|
||||||
|
dune exec bench/bench.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
`coolc`는 `_build/default/bin/main.exe`다. 설치하려면 `dune install`.
|
||||||
|
|
||||||
|
## 저장소 구성
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/ 컴파일러 (약 4,900줄 OCaml)
|
||||||
|
lexer.ml 어휘 분석 — 문법을 하나도 모른다
|
||||||
|
parser.ml 재귀 하강, LL(1), backtracking 없음
|
||||||
|
resolve.ml 이름 해소 — 모듈 하나만 보고 결정할 수 있는 것
|
||||||
|
typecheck.ml 타입 + effect + capability
|
||||||
|
move.ml move / affinity
|
||||||
|
exhaust.ml 소진성 (Maranget usefulness)
|
||||||
|
iface.ml interface artifact + 해시
|
||||||
|
session.ml 모듈 로딩 + 고정점 invalidation
|
||||||
|
ir.ml 얇은 typed IR
|
||||||
|
interp.ml 트리 워킹 인터프리터
|
||||||
|
std/ 표준 라이브러리 (본문 없는 선언, 런타임이 구현)
|
||||||
|
samples/ 예제 — 통과용 9개, 일부러 틀린 것 7개
|
||||||
|
bench/ 증분 루프 측정
|
||||||
|
```
|
||||||
|
|
||||||
|
## v0의 성격
|
||||||
|
|
||||||
|
이것은 쓸 수 있는 언어가 아니라 **설계가 옳은지 증명된 언어**다.
|
||||||
|
되돌리기 비싼 결정 — 문법, 타입, effect, capability, 소유권, 증분
|
||||||
|
아키텍처 — 이 전부 코드와 테스트로 못 박혔고, 빠른 검증 루프라는 시스템
|
||||||
|
속성이 측정으로 증명됐다.
|
||||||
|
|
||||||
|
그래서 이 코드를 통째로 버리고 v1로 번역해도 잃을 것이 없다. 애초에
|
||||||
|
그것이 v0의 목적이었다.
|
||||||
|
|
||||||
|
아직 없는 것: 진짜 컴파일(해석 실행만 한다), 병렬 실행(`scope`는 순차),
|
||||||
|
파일·네트워크 IO, 완전한 제네릭, 에디터 지원, 패키지 관리자. 표준
|
||||||
|
라이브러리는 `len`, `each`, `map`, `concat`, `show` 수준이다.
|
||||||
|
|
||||||
|
## 이름
|
||||||
|
|
||||||
|
언어는 coollang, 툴체인 바이너리는 `coolc`. 소스 확장자는 `.cool`.
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
(* 빠른 검증 루프는 주장이 아니라 측정이다.
|
||||||
|
|
||||||
|
측정하는 것은 두 가지다:
|
||||||
|
1. 전체 검사 시간 (cold) — 규모가 커져도 파국이 아닌가
|
||||||
|
2. 증분 재검사 시간 (warm) — 그리고 무엇이 재검사되었는가
|
||||||
|
|
||||||
|
두 번째가 본체다. 아키텍처의 주장은 "빠르다"가 아니라 "다시 볼 것이
|
||||||
|
적다"이고, 그것은 시간이 아니라 재검사된 모듈 수로 먼저 증명된다.
|
||||||
|
시간은 그 수가 옳다는 것의 따름 결과다. *)
|
||||||
|
|
||||||
|
open Coollang
|
||||||
|
|
||||||
|
let modules = 200
|
||||||
|
let fns_per_module = 68
|
||||||
|
let now () = Unix.gettimeofday ()
|
||||||
|
|
||||||
|
let find_sub hay needle =
|
||||||
|
let n = String.length needle and h = String.length hay in
|
||||||
|
let rec go i =
|
||||||
|
if i + n > h then failwith "not found"
|
||||||
|
else if String.sub hay i n = needle then i
|
||||||
|
else go (i + 1)
|
||||||
|
in
|
||||||
|
go 0
|
||||||
|
|
||||||
|
let write file s =
|
||||||
|
let oc = open_out_bin file in
|
||||||
|
output_string oc s;
|
||||||
|
close_out oc
|
||||||
|
|
||||||
|
(* i번 모듈은 i-1번 모듈을 가져온다. 사슬이므로 시그니처 변경은 끝까지
|
||||||
|
전파되어야 하고, 본문 변경은 한 칸도 가면 안 된다. *)
|
||||||
|
let gen_module i ~body =
|
||||||
|
let b = Buffer.create 8192 in
|
||||||
|
if i > 0 then
|
||||||
|
Buffer.add_string b (Printf.sprintf "import \"m%d\" as Up\n\n" (i - 1));
|
||||||
|
Buffer.add_string b "pub enum Shape {\n Circle(Int),\n Square(Int),\n}\n\n";
|
||||||
|
Buffer.add_string b "pub copyable struct Point {\n x: Int,\n y: Int,\n}\n\n";
|
||||||
|
Buffer.add_string b
|
||||||
|
(Printf.sprintf
|
||||||
|
"pub fn area(s: Shape) -> Int {\n\
|
||||||
|
\ match s {\n\
|
||||||
|
\ Circle(r) => r * r,\n\
|
||||||
|
\ Square(w) => w * w,\n\
|
||||||
|
\ }\n\
|
||||||
|
}\n\n");
|
||||||
|
if i > 0 then
|
||||||
|
Buffer.add_string b
|
||||||
|
"pub fn up_area(s: Up.Shape) -> Int {\n\
|
||||||
|
\ match s {\n\
|
||||||
|
\ Up.Circle(r) => Up.area(s),\n\
|
||||||
|
\ Up.Square(w) => w * w,\n\
|
||||||
|
\ }\n\
|
||||||
|
}\n\n";
|
||||||
|
for k = 0 to fns_per_module - 1 do
|
||||||
|
Buffer.add_string b
|
||||||
|
(Printf.sprintf
|
||||||
|
"pub fn f%d(p: Point, n: Int) -> Int {\n\
|
||||||
|
\ let a = p.x + n\n\
|
||||||
|
\ let b = p.y * %d\n\
|
||||||
|
\ let c = if a > b { a } else { b }\n\
|
||||||
|
\ %s\n\
|
||||||
|
}\n\n"
|
||||||
|
k (k + 1) body)
|
||||||
|
done;
|
||||||
|
Buffer.contents b
|
||||||
|
|
||||||
|
let count_lines s =
|
||||||
|
String.fold_left (fun n c -> if c = '\n' then n + 1 else n) 0 s
|
||||||
|
|
||||||
|
let ms t = Printf.sprintf "%.1fms" (t *. 1000.)
|
||||||
|
|
||||||
|
let () =
|
||||||
|
let dir = Filename.concat (Filename.get_temp_dir_name ()) "cool_bench" in
|
||||||
|
ignore
|
||||||
|
(Sys.command
|
||||||
|
(Printf.sprintf "rm -rf %s && mkdir -p %s" (Filename.quote dir)
|
||||||
|
(Filename.quote dir)));
|
||||||
|
let path i = Filename.concat dir (Printf.sprintf "m%d.cool" i) in
|
||||||
|
let lines = ref 0 in
|
||||||
|
for i = 0 to modules - 1 do
|
||||||
|
let s = gen_module i ~body:"a + c" in
|
||||||
|
lines := !lines + count_lines s;
|
||||||
|
write (path i) s
|
||||||
|
done;
|
||||||
|
Printf.printf "모듈 %d개, %d줄 생성\n" modules !lines;
|
||||||
|
|
||||||
|
(* 1. cold: 전체 그래프 로드와 검사 *)
|
||||||
|
let st = Session.create ~root:dir () in
|
||||||
|
let t0 = now () in
|
||||||
|
Session.load st (path (modules - 1));
|
||||||
|
let cold = now () -. t0 in
|
||||||
|
let errs = Session.errors st in
|
||||||
|
Printf.printf "cold 전체 검사 %s (오류 %d건)\n" (ms cold) (List.length errs);
|
||||||
|
if errs <> [] then
|
||||||
|
List.iter
|
||||||
|
(fun e -> prerr_endline (Session.string_of_error e))
|
||||||
|
(List.filteri (fun i _ -> i < 5) errs);
|
||||||
|
|
||||||
|
(* 2. warm: 사슬 한가운데 모듈의 본문만 수정 *)
|
||||||
|
let mid = modules / 2 in
|
||||||
|
write (path mid) (gen_module mid ~body:"a + c + 1");
|
||||||
|
let t0 = now () in
|
||||||
|
let touched = Session.recheck st [ path mid ] in
|
||||||
|
let warm_body = now () -. t0 in
|
||||||
|
Printf.printf "warm 본문만 수정 %s (재검사 %d개, 오류 %d건)\n" (ms warm_body)
|
||||||
|
(List.length touched)
|
||||||
|
(List.length (Session.errors st));
|
||||||
|
|
||||||
|
(* 3. warm: 같은 모듈의 시그니처 수정 — variant 추가 *)
|
||||||
|
let sig_src =
|
||||||
|
let s = gen_module mid ~body:"a + c + 1" in
|
||||||
|
let needle = " Square(Int),\n" in
|
||||||
|
let i = find_sub s needle in
|
||||||
|
String.sub s 0 (i + String.length needle)
|
||||||
|
^ " Tri(Int),\n"
|
||||||
|
^ String.sub s
|
||||||
|
(i + String.length needle)
|
||||||
|
(String.length s - i - String.length needle)
|
||||||
|
in
|
||||||
|
write (path mid) sig_src;
|
||||||
|
let t0 = now () in
|
||||||
|
let touched = Session.recheck st [ path mid ] in
|
||||||
|
let warm_sig = now () -. t0 in
|
||||||
|
let errs = Session.errors st in
|
||||||
|
Printf.printf "warm 시그니처 수정 %s (재검사 %d개, 오류 %d건)\n" (ms warm_sig)
|
||||||
|
(List.length touched) (List.length errs);
|
||||||
|
List.iter (fun e -> print_endline (" " ^ Session.string_of_error e)) errs
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
(executable
|
||||||
|
(name bench)
|
||||||
|
(libraries coollang unix))
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
(executable
|
(executable
|
||||||
(name main)
|
(name main)
|
||||||
(public_name cool)
|
(public_name coolc)
|
||||||
(libraries coollang))
|
(libraries coollang))
|
||||||
|
|||||||
+78
-8
@@ -2,12 +2,14 @@ let usage =
|
|||||||
{|coollang toolchain
|
{|coollang toolchain
|
||||||
|
|
||||||
사용법:
|
사용법:
|
||||||
cool check <file.cool>... 타입/effect/capability 검사 (fast path)
|
coolc check <file.cool>... 타입/effect/capability 검사 (import를 따라 모듈 그래프 전체)
|
||||||
cool run <file.cool> typed IR 인터프리터로 실행
|
coolc test <file.cool> [필터] 모듈 그래프의 test 블록 실행
|
||||||
cool tokens <file.cool> 토큰 덤프 (렉서 디버깅)
|
coolc iface <file.cool> interface 표면과 해시 출력
|
||||||
cool ast <file.cool> 구문 트리 덤프 (파서 디버깅)
|
coolc run <file.cool> [인자...] typed IR 인터프리터로 실행
|
||||||
cool deps <file.cool> 외부 참조 목록 (모듈의 의존 표면)
|
coolc tokens <file.cool> 토큰 덤프 (렉서 디버깅)
|
||||||
cool version 버전 출력
|
coolc ast <file.cool> 구문 트리 덤프 (파서 디버깅)
|
||||||
|
coolc deps <file.cool> 외부 참조 목록 (모듈의 의존 표면)
|
||||||
|
coolc version 버전 출력
|
||||||
|}
|
|}
|
||||||
|
|
||||||
let report_errors errors =
|
let report_errors errors =
|
||||||
@@ -32,6 +34,33 @@ let dump_ast file =
|
|||||||
m.Coollang.Ast.items;
|
m.Coollang.Ast.items;
|
||||||
0
|
0
|
||||||
|
|
||||||
|
(* import를 따라 모듈 그래프를 로드하고 전부 검사한다. root는 첫 파일의 디렉터리다. *)
|
||||||
|
let check_graph files =
|
||||||
|
match files with
|
||||||
|
| [] ->
|
||||||
|
prerr_endline "검사할 파일이 없습니다";
|
||||||
|
2
|
||||||
|
| first :: _ ->
|
||||||
|
let st = Coollang.Session.create ~root:(Filename.dirname first) () in
|
||||||
|
List.iter (fun f -> Coollang.Session.load st f) files;
|
||||||
|
let errors = Coollang.Session.errors st in
|
||||||
|
List.iter
|
||||||
|
(fun e -> prerr_endline (Coollang.Session.string_of_error e))
|
||||||
|
errors;
|
||||||
|
if errors = [] then 0 else 1
|
||||||
|
|
||||||
|
let dump_iface file =
|
||||||
|
let st = Coollang.Session.create ~root:(Filename.dirname file) () in
|
||||||
|
Coollang.Session.load st file;
|
||||||
|
match Coollang.Session.find st file with
|
||||||
|
| None -> 1
|
||||||
|
| Some e ->
|
||||||
|
Printf.printf "hash %s\n" e.iface.Coollang.Iface.hash;
|
||||||
|
List.iter
|
||||||
|
(fun it -> print_endline (Coollang.Ast.show_item it))
|
||||||
|
e.iface.Coollang.Iface.items;
|
||||||
|
0
|
||||||
|
|
||||||
let dump_deps file =
|
let dump_deps file =
|
||||||
match Coollang.Driver.resolve file with
|
match Coollang.Driver.resolve file with
|
||||||
| Error errors -> report_errors errors
|
| Error errors -> report_errors errors
|
||||||
@@ -46,8 +75,49 @@ let () =
|
|||||||
let argv = Array.to_list Sys.argv in
|
let argv = Array.to_list Sys.argv in
|
||||||
let code =
|
let code =
|
||||||
match List.tl argv with
|
match List.tl argv with
|
||||||
| "check" :: files -> report (Coollang.Driver.check files)
|
| "check" :: files -> check_graph files
|
||||||
| [ "run"; file ] -> report (Coollang.Driver.run file)
|
| [ "iface"; file ] -> dump_iface file
|
||||||
|
| "test" :: file :: rest -> (
|
||||||
|
let filter = match rest with f :: _ -> f | [] -> "" in
|
||||||
|
let st = Coollang.Session.create ~root:(Filename.dirname file) () in
|
||||||
|
match Coollang.Session.test ~filter st file with
|
||||||
|
| Error errors ->
|
||||||
|
List.iter
|
||||||
|
(fun e -> prerr_endline (Coollang.Session.string_of_error e))
|
||||||
|
errors;
|
||||||
|
1
|
||||||
|
| Ok results ->
|
||||||
|
let failed =
|
||||||
|
List.filter
|
||||||
|
(fun (r : Coollang.Interp.test_result) -> r.t_failure <> None)
|
||||||
|
results
|
||||||
|
in
|
||||||
|
List.iter
|
||||||
|
(fun (r : Coollang.Interp.test_result) ->
|
||||||
|
match r.t_failure with
|
||||||
|
| None -> ()
|
||||||
|
| Some (p, msg) ->
|
||||||
|
Printf.printf "FAIL %s:%d %s\n %s\n" r.t_module
|
||||||
|
r.t_pos.line r.t_name msg;
|
||||||
|
ignore p)
|
||||||
|
results;
|
||||||
|
Printf.printf "%s 테스트 %d개 중 %d개 통과\n"
|
||||||
|
(if failed = [] then "ok " else "실패")
|
||||||
|
(List.length results)
|
||||||
|
(List.length results - List.length failed);
|
||||||
|
if failed = [] then 0 else 1)
|
||||||
|
| "run" :: file :: args -> (
|
||||||
|
let st = Coollang.Session.create ~root:(Filename.dirname file) () in
|
||||||
|
match Coollang.Session.run ~args st file with
|
||||||
|
| out, None ->
|
||||||
|
print_string out;
|
||||||
|
0
|
||||||
|
| out, Some e ->
|
||||||
|
(* 실패해도 그때까지의 출력을 먼저 보여준다 *)
|
||||||
|
print_string out;
|
||||||
|
flush stdout;
|
||||||
|
prerr_endline (Coollang.Session.string_of_error e);
|
||||||
|
1)
|
||||||
| [ "tokens"; file ] -> dump_tokens file
|
| [ "tokens"; file ] -> dump_tokens file
|
||||||
| [ "ast"; file ] -> dump_ast file
|
| [ "ast"; file ] -> dump_ast file
|
||||||
| [ "deps"; file ] -> dump_deps file
|
| [ "deps"; file ] -> dump_deps 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의 질문은 "되돌리기 비싼 결정이 옳은가"였다. 답은 **그렇다**이고,
|
||||||
|
남은 불편은 전부 되돌리기 싼 것들이다.
|
||||||
+193
-83
@@ -7,10 +7,24 @@
|
|||||||
* { } 반복 (0 이상)
|
* { } 반복 (0 이상)
|
||||||
* ( ) 묶음
|
* ( ) 묶음
|
||||||
* " " 단말
|
* " " 단말
|
||||||
|
* < > 매개변수 (아래 참고)
|
||||||
* (* *) 주석
|
* (* *) 주석
|
||||||
*
|
*
|
||||||
* 설계 제약: LL(1). backtracking 없음, 렉서 피드백 없음.
|
* 설계 제약: LL(1). backtracking 없음, 렉서 피드백 없음.
|
||||||
* 어떤 프로덕션도 무한 선읽기를 요구해서는 안 된다 (철학 2).
|
* 어떤 프로덕션도 무한 선읽기를 요구해서는 안 된다 (철학 2).
|
||||||
|
* 이 제약은 주장이 아니라 검사된다 — tools/ebnf_tool.exe가 이 파일을 읽어
|
||||||
|
* FIRST/FOLLOW를 계산하고 충돌을 보고하며, 충돌이 있으면 테스트가 깨진다.
|
||||||
|
*
|
||||||
|
* 매개변수 프로덕션:
|
||||||
|
* name<p> = ... p ... 로 정의하고 name<arg> 로 참조한다.
|
||||||
|
* 인자마다 하나의 평범한 프로덕션으로 펼쳐진다. 인자에 따라 몸통이 달라야
|
||||||
|
* 하면 머리에 인자를 박아 특수화한다 (name<yes> = ... ; name<no> = ... ;).
|
||||||
|
* 목록과 expr_ns를 복제 없이 적기 위한 것이다.
|
||||||
|
*
|
||||||
|
* 흡수 규칙:
|
||||||
|
* [ NEWLINE ]은 최대한 먹는다(greedy). 줄바꿈은 값을 갖지 않으므로 바깥
|
||||||
|
* 프로덕션이 먹든 안쪽이 먹든 파스 트리가 같다. 이 규칙이 없으면 흡수
|
||||||
|
* 지점마다 형식적 중의성이 생기지만, 어느 쪽으로 읽어도 결과가 같다.
|
||||||
*)
|
*)
|
||||||
|
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
@@ -20,43 +34,84 @@
|
|||||||
(* 주석은 // 부터 줄 끝까지. 블록 주석 없음 (중첩 규칙이라는 변종을 만들지 않는다) *)
|
(* 주석은 // 부터 줄 끝까지. 블록 주석 없음 (중첩 규칙이라는 변종을 만들지 않는다) *)
|
||||||
|
|
||||||
(* 문 구분자 NEWLINE은 렉서가 삽입한다:
|
(* 문 구분자 NEWLINE은 렉서가 삽입한다:
|
||||||
* 줄의 마지막 토큰이 ident, 리터럴, ")", "]", "}", "?", "return" 중 하나이면
|
* 줄의 마지막 토큰이 값으로 끝날 수 있는 토큰이면 그 줄 끝에 NEWLINE을 넣는다.
|
||||||
* 그 줄 끝에 NEWLINE 토큰을 넣는다. 그 외에는 넣지 않는다.
|
* 그 목록은 아래 생성 블록에 있다 — 손으로 적지 않고 코드에서 뽑는다.
|
||||||
* 따라서 연산자나 여는 괄호로 끝나는 줄은 다음 줄로 이어진다.
|
* 그 외에는 넣지 않는다. 따라서 연산자나 여는 괄호로 끝나는 줄은 이어진다.
|
||||||
* 빈 줄과 주석만 있는 줄은 NEWLINE을 만들지 않는다.
|
* 빈 줄과 주석만 있는 줄은 NEWLINE을 만들지 않는다.
|
||||||
*
|
*
|
||||||
|
* NEWLINE은 결코 연달아 나오지 않는다. 삽입 판정이 "마지막으로 낸 토큰"을
|
||||||
|
* 보는데 NEWLINE 자신은 값으로 끝날 수 있는 토큰이 아니기 때문이다.
|
||||||
|
* 그래서 이 문법의 모든 흡수 지점은 { NEWLINE }이 아니라 [ NEWLINE ]이다.
|
||||||
|
*
|
||||||
|
(* 여기부터 lib/token.ml에서 생성됩니다 — 손으로 고치지 마십시오 *)
|
||||||
|
* 줄을 끝낼 수 있는 토큰. 줄의 마지막 토큰이 이 중 하나이면 그 줄 끝에
|
||||||
|
* NEWLINE이 삽입된다. Token.can_end_statement가 원본이다.
|
||||||
|
*
|
||||||
|
* ident | int_lit | string_lit | "return" | "true" | "false" | ")" |
|
||||||
|
* "}" | "]" | "?"
|
||||||
|
*
|
||||||
|
* 키워드. 이름으로 쓸 수 없다. Token.keyword가 원본이다.
|
||||||
|
*
|
||||||
|
* "pub" | "fn" | "struct" | "enum" | "capability" | "const" |
|
||||||
|
* "import" | "as" | "reexport" | "let" | "mut" | "own" | "self" |
|
||||||
|
* "affine" | "copyable" | "effects" | "return" | "if" | "else" |
|
||||||
|
* "match" | "scope" | "crash" | "test" | "true" | "false"
|
||||||
|
(* 생성 끝 *)
|
||||||
|
*
|
||||||
* 다중 줄 목록(파라미터, 인자, 필드, variant, 리스트 리터럴)은 후행 콤마가
|
* 다중 줄 목록(파라미터, 인자, 필드, variant, 리스트 리터럴)은 후행 콤마가
|
||||||
* 필수다. 콤마로 끝난 줄은 NEWLINE을 만들지 않으므로 목록이 자연히 이어진다.
|
* 필수다. 콤마로 끝난 줄은 NEWLINE을 만들지 않으므로 목록이 자연히 이어진다.
|
||||||
* 공식 formatter가 이를 강제한다.
|
* 공식 formatter가 이를 강제한다.
|
||||||
*
|
*
|
||||||
* 중괄호 목록(struct 정의, enum 정의, struct 리터럴, effect 집합, match 팔)
|
|
||||||
* 안에서는 항목 구분자 주변의 NEWLINE이 무시된다. 문이 놓이는 자리가 아니므로
|
|
||||||
* 구분자로 쓰이지 않는다. 아래 프로덕션에 { NEWLINE }으로 적혀 있다.
|
|
||||||
*
|
|
||||||
* 시그니처 머리에서는 NEWLINE이 문법적으로 허용되고 무시된다. effects 절이
|
* 시그니처 머리에서는 NEWLINE이 문법적으로 허용되고 무시된다. effects 절이
|
||||||
* 줄 끝에 오면 "}"가 값 종료 토큰이라 NEWLINE이 삽입되는데, 이 자리는 문이
|
* 줄 끝에 오면 "}"가 값 종료 토큰이라 NEWLINE이 삽입되는데, 이 자리는 문이
|
||||||
* 끝날 수 있는 자리가 아니므로 아래 프로덕션이 { NEWLINE }으로 흡수한다.
|
* 끝날 수 있는 자리가 아니므로 아래 프로덕션이 [ NEWLINE ]으로 흡수한다.
|
||||||
* 흡수 위치를 프로덕션에 명시적으로 적는다 — 파서가 임의로 건너뛰지 않는다.
|
* 흡수 위치를 프로덕션에 명시적으로 적는다 — 파서가 임의로 건너뛰지 않는다.
|
||||||
*)
|
*)
|
||||||
|
|
||||||
ident = letter , { letter | digit | "_" } ;
|
ident = letter , { letter | digit | "_" } ;
|
||||||
int_lit = digit , { digit | "_" } ;
|
int_lit = digit , { digit | "_" } ;
|
||||||
string_lit = '"' , { char - '"' } , '"' ;
|
string_lit = '"' , { str_char } , '"' ;
|
||||||
|
str_char = ( char - '"' - "\" ) | escape ;
|
||||||
|
escape = "\" , ( "n" | "t" | "\" | '"' ) ;
|
||||||
bool_lit = "true" | "false" ;
|
bool_lit = "true" | "false" ;
|
||||||
literal = int_lit | string_lit | bool_lit ;
|
literal = int_lit | string_lit | bool_lit ;
|
||||||
|
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
(* 목록 *)
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
(* 후행 콤마를 허용하는 목록. 우재귀로 적는 이유는 LL(1)이다 —
|
||||||
|
* X , { "," , X } , [ "," ] 로 적으면 콤마를 본 시점에 "항목이 더 있는지"와
|
||||||
|
* "이게 후행 콤마인지"가 갈리지 않는다. 콤마를 먹은 뒤 닫는 토큰을 보고
|
||||||
|
* 갈리는 것이 파서가 실제로 하는 일이고, 아래가 그것이다. *)
|
||||||
|
list<item> = item , list_rest<item> ;
|
||||||
|
list_rest<item> = [ "," , [ list<item> ] ] ;
|
||||||
|
|
||||||
|
(* 중괄호 안의 목록. 항목 사이에 줄바꿈이 올 수 있다는 점만 다르다.
|
||||||
|
* 항목 사이의 콤마는 필수이고 마지막 항목 뒤에서만 생략된다. *)
|
||||||
|
brace_list<item> = [ NEWLINE ] , [ item , brace_rest<item> ] ;
|
||||||
|
brace_rest<item> = [ NEWLINE ] ,
|
||||||
|
[ "," , [ NEWLINE ] , [ item , brace_rest<item> ] ] ;
|
||||||
|
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
(* 모듈 *)
|
(* 모듈 *)
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
module = { NEWLINE } , { item } ;
|
module = [ NEWLINE ] , { item } ;
|
||||||
item = ( import | reexport | decl ) , { NEWLINE } ;
|
item = ( import | reexport | test_decl | decl ) , [ NEWLINE ] ;
|
||||||
(* decl이 이미 NEWLINE을 흡수했을 수 있으므로 항목 구분자는 0개 이상이다.
|
|
||||||
* 문 수준에서는 그렇지 않다 — stmt는 NEWLINE 하나를 반드시 요구한다 *)
|
|
||||||
|
|
||||||
import = "import" , string_lit , "as" , ident ;
|
import = "import" , string_lit , "as" , ident ;
|
||||||
reexport = "reexport" , ident ;
|
reexport = "reexport" , ident ;
|
||||||
|
|
||||||
|
(* 테스트는 프로그램의 일부이고 같은 검사를 받는다. pub이 없다 — 밖에서
|
||||||
|
* 부르는 것이 아니다. 이름이 필수인 이유는 실패했을 때 무엇이 깨졌는지
|
||||||
|
* 말해야 하기 때문이다.
|
||||||
|
* 파라미터가 없으므로 capability를 받을 수 없고, capability를 만드는 문법도
|
||||||
|
* 없다. 따라서 테스트는 effect-free임이 증명된다 — 관례가 아니라 검사다.
|
||||||
|
* 그 결과 순서에 의존하지 않고, 병렬로 돌려도 같고, 캐시할 수 있다.
|
||||||
|
* interface hash에는 들어가지 않는다 (함수 본문과 같은 이유) *)
|
||||||
|
test_decl = "test" , string_lit , block ;
|
||||||
|
|
||||||
decl = [ "pub" ] , ( fn_decl | struct_decl | enum_decl
|
decl = [ "pub" ] , ( fn_decl | struct_decl | enum_decl
|
||||||
| capability_decl | const_decl ) ;
|
| capability_decl | const_decl ) ;
|
||||||
|
|
||||||
@@ -65,53 +120,62 @@ decl = [ "pub" ] , ( fn_decl | struct_decl | enum_decl
|
|||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
fn_decl = "fn" , ident , [ gen_params ] , "(" , [ params ] , ")" ,
|
fn_decl = "fn" , ident , [ gen_params ] , "(" , [ params ] , ")" ,
|
||||||
{ NEWLINE } ,
|
[ NEWLINE ] ,
|
||||||
[ eff_result , { NEWLINE } ] ,
|
[ eff_result , [ NEWLINE ] ] ,
|
||||||
[ "->" , type , { NEWLINE } ] ,
|
[ "->" , type , [ NEWLINE ] ] ,
|
||||||
[ block ] ;
|
[ block ] ;
|
||||||
(* block이 없으면 시그니처 선언. interface 파일과 capability 본문에서 쓴다 *)
|
(* block이 없으면 시그니처 선언. std/*.cool과 capability 본문에서 쓴다 *)
|
||||||
|
|
||||||
struct_decl = [ "copyable" ] , "struct" , ident , [ gen_params ] ,
|
struct_decl = [ "copyable" ] , "struct" , ident , [ gen_params ] ,
|
||||||
"{" , { field } , "}" ;
|
"{" , brace_list<field> , "}" ;
|
||||||
field = ident , ":" , type , { NEWLINE } ,
|
field = ident , ":" , type ;
|
||||||
[ "," , { NEWLINE } ] ;
|
|
||||||
|
|
||||||
enum_decl = "enum" , ident , [ gen_params ] , "{" , { variant } , "}" ;
|
enum_decl = "enum" , ident , [ gen_params ] ,
|
||||||
variant = ident , [ "(" , type_list , ")" ] , { NEWLINE } ,
|
"{" , brace_list<variant> , "}" ;
|
||||||
[ "," , { NEWLINE } ] ;
|
variant = ident , [ "(" , type_list , ")" ] ;
|
||||||
|
|
||||||
capability_decl = "capability" , ident , "{" , { NEWLINE } , { cap_method } , "}" ;
|
capability_decl = "capability" , ident , "{" , [ NEWLINE ] , { cap_method } , "}" ;
|
||||||
cap_method = "fn" , ident , "(" , [ params ] , ")" , { NEWLINE } ,
|
cap_method = "fn" , ident , [ gen_params ] , "(" , [ cap_params ] , ")" ,
|
||||||
[ eff_result , { NEWLINE } ] , [ "->" , type ] ,
|
[ NEWLINE ] ,
|
||||||
{ NEWLINE } ;
|
[ eff_result , [ NEWLINE ] ] ,
|
||||||
|
[ "->" , type ] ,
|
||||||
|
[ NEWLINE ] ;
|
||||||
|
|
||||||
|
(* capability 메서드만 첫 자리에 수신자를 적을 수 있다.
|
||||||
|
*
|
||||||
|
* 빌리는 것이 기본이므로 빌림에는 적을 말이 없다 — `self`만 적는 문장은
|
||||||
|
* 존재하지 않는다. 뜻이 있는 쪽에만 표기가 있다.
|
||||||
|
*
|
||||||
|
* 일반 fn에는 이 자리가 없다. 검사기가 거부하는 것이 아니라 문장이
|
||||||
|
* 존재하지 않는다.
|
||||||
|
*
|
||||||
|
* "own"을 먼저 먹고 그 다음을 보므로 선읽기는 여전히 한 토큰이다 *)
|
||||||
|
cap_params = "own" , cap_own_rest
|
||||||
|
| plain_param , list_rest<param> ;
|
||||||
|
cap_own_rest = "self" , list_rest<param>
|
||||||
|
| plain_param , list_rest<param> ;
|
||||||
|
|
||||||
const_decl = "const" , ident , ":" , type , "=" , expr ;
|
const_decl = "const" , ident , ":" , type , "=" , expr ;
|
||||||
|
|
||||||
gen_params = "[" , gen_param , { "," , gen_param } , [ "," ] , "]" ;
|
gen_params = "[" , list<gen_param> , "]" ;
|
||||||
gen_param = ident , [ ":" , "effects" ] ;
|
gen_param = ident , [ ":" , "effects" ] ;
|
||||||
(* ident 단독 = 타입 파라미터, ": effects" = effect 파라미터 *)
|
params = list<param> ;
|
||||||
|
param = [ "own" ] , plain_param ;
|
||||||
params = param , { "," , param } , [ "," ] ;
|
plain_param = [ "mut" ] , ident , ":" , type ;
|
||||||
param = [ "own" ] , [ "mut" ] , ident , ":" , type ;
|
|
||||||
(* 무표기 = use(빌림). own만이 소유 이전을 뜻한다.
|
|
||||||
* 수식어 순서 고정: 바인딩 수식어(own, mut)가 먼저, 타입 수식어(affine)는 type 안 *)
|
|
||||||
|
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
(* effect 절 *)
|
(* effect *)
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
(* 결과 위치 — 함수 선언 자신의 effect. 합집합 허용 *)
|
(* 결과 위치에서만 합집합이 가능하다 *)
|
||||||
eff_result = "effects" , eff_union ;
|
eff_result = "effects" , eff_atom , { "|" , eff_atom } ;
|
||||||
eff_union = eff_atom , { "|" , eff_atom } ;
|
|
||||||
|
|
||||||
(* 파라미터 위치 — 함수 타입 안의 effect. 합집합이 문법에 없다.
|
(* 파라미터 위치. 합집합이 없다 — 문법에 그런 문장이 존재하지 않는다.
|
||||||
* "검사기가 거부"가 아니라 "그런 문장이 존재하지 않음"이다 *)
|
* "검사기가 거부"가 아니라 "쓸 수 없다"이다 *)
|
||||||
eff_param = "effects" , eff_atom ;
|
eff_param = "effects" , eff_atom ;
|
||||||
|
|
||||||
eff_atom = ident | eff_set ;
|
eff_atom = ident | eff_set ;
|
||||||
eff_set = "{" , { NEWLINE } ,
|
eff_set = "{" , brace_list<eff_name> , "}" ;
|
||||||
[ eff_name , { { NEWLINE } , "," , { NEWLINE } , eff_name } ,
|
|
||||||
{ NEWLINE } , [ "," , { NEWLINE } ] ] , "}" ;
|
|
||||||
eff_name = ident , "." , ident ;
|
eff_name = ident , "." , ident ;
|
||||||
(* 타입 수준 이름만. capability 값의 identity는 정적 층에 등장하지 않는다 *)
|
(* 타입 수준 이름만. capability 값의 identity는 정적 층에 등장하지 않는다 *)
|
||||||
|
|
||||||
@@ -121,31 +185,41 @@ eff_name = ident , "." , ident ;
|
|||||||
|
|
||||||
type = fn_type | named_type ;
|
type = fn_type | named_type ;
|
||||||
|
|
||||||
fn_type = [ "affine" ] , "fn" , "(" , [ type_list ] , ")" ,
|
(* 함수 타입의 파라미터에도 own을 적는다. 이것이 없으면 "소유권을 가져가는
|
||||||
|
* 클로저"를 타입으로 표현할 수 없고, 고차 경계에서 소유권 검사가 뚫린다 *)
|
||||||
|
fn_type = [ "affine" ] , "fn" , "(" , [ list<fn_param_ty> ] , ")" ,
|
||||||
[ eff_param ] , [ "->" , type ] ;
|
[ eff_param ] , [ "->" , type ] ;
|
||||||
|
fn_param_ty = [ "own" ] , type ;
|
||||||
|
|
||||||
named_type = ident , [ type_args ] ;
|
(* 다른 모듈의 타입은 별칭으로 한정한다: Shapes.Shape.
|
||||||
type_args = "[" , targ , { "," , targ } , [ "," ] , "]" ;
|
* 한 단계뿐이다 — 별칭은 이 모듈의 이름이므로 더 이어질 자리가 없다 *)
|
||||||
|
named_type = ident , [ "." , ident ] , [ type_args ] ;
|
||||||
|
type_args = "[" , list<targ> , "]" ;
|
||||||
targ = type | eff_set ;
|
targ = type | eff_set ;
|
||||||
(* 제네릭 인자는 타입 또는 effect다. 맨 이름은 둘 다일 수 있으므로 파서는
|
(* 제네릭 인자는 타입 또는 effect다. 맨 이름은 둘 다일 수 있으므로 파서는
|
||||||
* 타입으로 읽고 이름 해소가 판정한다 — 구문 층에서 갈리지 않아도 된다 *)
|
* 타입으로 읽고 이름 해소가 판정한다 — 구문 층에서 갈리지 않아도 된다 *)
|
||||||
type_list = type , { "," , type } , [ "," ] ;
|
type_list = list<type> ;
|
||||||
|
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
(* 문과 블록 *)
|
(* 문과 블록 *)
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
block = "{" , { NEWLINE } ,
|
block = "{" , [ NEWLINE ] , [ stmt , stmt_rest ] , "}" ;
|
||||||
[ stmt , { stmt_sep , stmt } , [ stmt_sep ] ] , "}" ;
|
stmt_rest = [ NEWLINE , [ stmt , stmt_rest ] ] ;
|
||||||
stmt = let_stmt | return_stmt | assign_stmt | expr ;
|
(* 문 사이의 NEWLINE은 필수다. 선택적으로 적으면 식이 식 뒤에 바로 올 수
|
||||||
stmt_sep = NEWLINE , { NEWLINE } ;
|
* 있게 되고, 그러면 "-"나 "("로 시작하는 다음 문과 앞 식의 이어짐이
|
||||||
(* 마지막 문의 구분자는 "}" 앞에서 생략된다.
|
* 갈리지 않는다 — 한 토큰으로 결정할 수 없게 된다.
|
||||||
|
* 마지막 문의 구분자는 "}" 앞에서 생략된다.
|
||||||
* fn(s) { String.concat(prefix, s) } 처럼 한 줄로 쓰는 자리가 있기 때문이다 *)
|
* fn(s) { String.concat(prefix, s) } 처럼 한 줄로 쓰는 자리가 있기 때문이다 *)
|
||||||
|
|
||||||
|
stmt = let_stmt | return_stmt | expr_stmt ;
|
||||||
let_stmt = "let" , [ "mut" ] , pattern , [ ":" , type ] , "=" , expr ;
|
let_stmt = "let" , [ "mut" ] , pattern , [ ":" , type ] , "=" , expr ;
|
||||||
return_stmt = "return" , [ expr ] ;
|
return_stmt = "return" , [ expr ] ;
|
||||||
assign_stmt = place , "=" , expr ;
|
(* 값 생략은 다음 토큰이 NEWLINE이거나 "}"일 때다 *)
|
||||||
place = ident , { "." , ident } ;
|
|
||||||
|
expr_stmt = expr , [ "=" , expr ] ;
|
||||||
|
(* 대입 왼쪽에 올 수 있는 것(이름 또는 필드 접근)은 구문이 아니라 정적 검사가
|
||||||
|
* 판정한다. 구문으로 가르면 ident 하나로 대입과 식이 갈리지 않는다 *)
|
||||||
|
|
||||||
(* 블록의 값 = 마지막 stmt가 expr이면 그 값, 아니면 Unit.
|
(* 블록의 값 = 마지막 stmt가 expr이면 그 값, 아니면 Unit.
|
||||||
* return은 조기 탈출 전용이며, 꼬리 위치의 return은 formatter가 지적한다 *)
|
* return은 조기 탈출 전용이며, 꼬리 위치의 return은 formatter가 지적한다 *)
|
||||||
@@ -154,62 +228,98 @@ place = ident , { "." , ident } ;
|
|||||||
(* 식 *)
|
(* 식 *)
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
expr = or_expr ;
|
(* 매개변수 s는 이 자리에서 struct 리터럴을 쓸 수 있는지다.
|
||||||
or_expr = and_expr , { "||" , and_expr } ;
|
* expr = 쓸 수 있는 자리 (거의 전부)
|
||||||
and_expr = cmp_expr , { "&&" , cmp_expr } ;
|
* expr_ns = 쓸 수 없는 자리 (if/match의 머리)
|
||||||
cmp_expr = add_expr , [ cmp_op , add_expr ] ;
|
* if/match의 머리에서 "{"가 블록의 시작인지 struct 리터럴인지 갈리지
|
||||||
cmp_op = "==" | "!=" | "<" | "<=" | ">" | ">=" ;
|
* 않으므로, 그 자리의 struct 리터럴은 괄호로 감싼다.
|
||||||
add_expr = mul_expr , { ( "+" | "-" ) , mul_expr } ;
|
* 제약은 머리 식의 최상위에만 걸린다 — 괄호, 대괄호, 블록, 호출 인자 등
|
||||||
mul_expr = unary , { ( "*" | "/" | "%" ) , unary } ;
|
* 새 구문 문맥에 들어가는 순간 풀린다. 그래서 primary<no>의 괄호 안이
|
||||||
unary = [ "!" | "-" ] , postfix ;
|
* expr(= yes)이다.
|
||||||
|
* scope의 머리에는 식이 없다(이름 둘뿐) — 제약이 걸릴 자리가 없다 *)
|
||||||
|
expr = or_expr<yes> ;
|
||||||
|
expr_ns = or_expr<no> ;
|
||||||
|
|
||||||
postfix = primary , { call_sfx | field_sfx | inst_sfx | "?" } ;
|
or_expr<s> = and_expr<s> , { "||" , and_expr<s> } ;
|
||||||
|
and_expr<s> = cmp_expr<s> , { "&&" , cmp_expr<s> } ;
|
||||||
|
cmp_expr<s> = add_expr<s> , [ cmp_op , add_expr<s> ] ;
|
||||||
|
cmp_op = "==" | "!=" | "<" | "<=" | ">" | ">=" ;
|
||||||
|
add_expr<s> = mul_expr<s> , { ( "+" | "-" ) , mul_expr<s> } ;
|
||||||
|
mul_expr<s> = unary<s> , { ( "*" | "/" | "%" ) , unary<s> } ;
|
||||||
|
unary<s> = [ "!" | "-" ] , postfix<s> ;
|
||||||
|
|
||||||
|
postfix<s> = primary<s> , { call_sfx | field_sfx | inst_sfx | "?" } ;
|
||||||
call_sfx = "(" , [ args ] , ")" ;
|
call_sfx = "(" , [ args ] , ")" ;
|
||||||
field_sfx = "." , ident ;
|
field_sfx = "." , ident ;
|
||||||
inst_sfx = type_args ;
|
inst_sfx = type_args ;
|
||||||
(* 후위 "[" = 명시적 인스턴스화, 전위 "[" = 리스트 리터럴. 위치가 결정한다 *)
|
(* 후위 "[" = 명시적 인스턴스화, 전위 "[" = 리스트 리터럴. 위치가 결정한다 *)
|
||||||
args = expr , { "," , expr } , [ "," ] ;
|
args = list<expr> ;
|
||||||
|
|
||||||
primary = literal
|
primary<s> = literal
|
||||||
| ident
|
|
||||||
| list_lit
|
| list_lit
|
||||||
| struct_lit
|
|
||||||
| closure
|
| closure
|
||||||
| if_expr
|
| if_expr
|
||||||
| match_expr
|
| match_expr
|
||||||
| scope_expr
|
| scope_expr
|
||||||
| "(" , expr , ")" ;
|
| "(" , expr , ")"
|
||||||
|
| crash_expr
|
||||||
|
| name_or_struct<s> ;
|
||||||
|
|
||||||
list_lit = "[" , [ expr , { "," , expr } , [ "," ] ] , "]" ;
|
(* crash는 복구 불가능한 결함이다. 되돌아올 수 없으므로 타입이 Never이고,
|
||||||
|
* Never는 어떤 타입 자리에도 놓일 수 있다 — 그래야 match 팔에서 쓸 수 있다.
|
||||||
|
*
|
||||||
|
* effect가 아니다. 경계 검사 하나 넣었다고 {Crash}가 호출자 전부로
|
||||||
|
* 전염되면 effect 절은 신호가 아니라 잡음이 된다. 발산이 effect가 아닌
|
||||||
|
* 것과 같은 이유다 — 무한 루프도 추적하지 않는다.
|
||||||
|
*
|
||||||
|
* 언어 수준 recover가 없다. 붙잡는 것이 있으면 그것은 예외이고, 예외는
|
||||||
|
* 시그니처에 안 적히므로 철학 1과 충돌한다. 런타임은 격리 경계를 가질 수
|
||||||
|
* 있다 (테스트 러너가 첫 사례).
|
||||||
|
*
|
||||||
|
* 되감기를 하지 않는다. crash 시 자원 해제 여부는 자원 모델과 함께
|
||||||
|
* 결정한다 — 지금은 열어둔다.
|
||||||
|
*
|
||||||
|
* 키워드인 이유: prelude가 없어서 함수로 두면 쓸 때마다 import해야 한다 *)
|
||||||
|
crash_expr = "crash" , "(" , expr , ")" ;
|
||||||
|
|
||||||
|
(* ident 하나로는 이름인지 struct 리터럴인지 갈리지 않는다. "{"를 보고
|
||||||
|
* 갈리므로 왼쪽으로 인수분해해 적는다 — 파서가 실제로 하는 일이다 *)
|
||||||
|
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] = [] *)
|
(* 빈 리스트는 타입 주석이 필요하다: let xs: List[Int] = [] *)
|
||||||
|
|
||||||
struct_lit = ident , "{" , { field_init } , "}" ;
|
|
||||||
field_init = ident , ":" , expr , { NEWLINE } , [ "," , { NEWLINE } ] ;
|
|
||||||
|
|
||||||
closure = "fn" , "(" , [ cl_params ] , ")" ,
|
closure = "fn" , "(" , [ cl_params ] , ")" ,
|
||||||
[ eff_param ] , [ "->" , type ] , block ;
|
[ eff_param ] , [ "->" , type ] , block ;
|
||||||
cl_params = cl_param , { "," , cl_param } , [ "," ] ;
|
cl_params = list<cl_param> ;
|
||||||
cl_param = ident , [ ":" , type ] ;
|
(* 클로저 파라미터의 소유권은 리터럴이 스스로 적는다. 타입은 기대 타입에서
|
||||||
|
* 읽어오지만 소유권은 읽어오지 않는다 — move 검사는 타입 검사와 별도 순회라
|
||||||
|
* 타입을 모르고, 소유권은 타입보다 결과가 크기 때문이다.
|
||||||
|
* 무표기는 빌림이다 (함수 파라미터와 같다) *)
|
||||||
|
cl_param = [ "own" ] , ident , [ ":" , type ] ;
|
||||||
(* 파라미터 타입 생략 가능. 호출 지점의 기대 타입에서 읽어온다 — 함수 로컬이다.
|
(* 파라미터 타입 생략 가능. 호출 지점의 기대 타입에서 읽어온다 — 함수 로컬이다.
|
||||||
* 기대 타입이 없는 자리에서 생략하면 error *)
|
* 기대 타입이 없는 자리에서 생략하면 error *)
|
||||||
|
|
||||||
if_expr = "if" , expr_ns , block , [ "else" , ( block | if_expr ) ] ;
|
if_expr = "if" , expr_ns , block , [ "else" , ( block | if_expr ) ] ;
|
||||||
match_expr = "match" , expr_ns , "{" , { arm } , "}" ;
|
match_expr = "match" , expr_ns , "{" , brace_list<arm> , "}" ;
|
||||||
arm = pattern , "=>" , ( expr | block ) , { NEWLINE } ,
|
arm = pattern , "=>" , ( expr | block ) ;
|
||||||
[ "," , { NEWLINE } ] ;
|
|
||||||
scope_expr = "scope" , ident , "=" , ident , block ;
|
scope_expr = "scope" , ident , "=" , ident , block ;
|
||||||
(* scope 자식 = 부모 { ... }
|
(* scope 자식 = 부모 { ... }
|
||||||
* 부모를 구문에 적는다. 적지 않으면 자식의 부모가 "가장 가까운 스코프"가 되어
|
* 부모를 구문에 적는다. 적지 않으면 자식의 부모가 "가장 가까운 스코프"가 되어
|
||||||
* 정확히 ambient authority가 된다 — 이 언어가 배제하는 것 *)
|
* 정확히 ambient authority가 된다 — 이 언어가 배제하는 것 *)
|
||||||
|
|
||||||
(* expr_ns = struct_lit로 시작하지 않는 expr.
|
|
||||||
* if/match/scope의 머리 자리에서 "{"가 블록의 시작인지 struct 리터럴인지
|
|
||||||
* 갈리지 않으므로, 그 자리의 struct 리터럴은 괄호로 감싼다 *)
|
|
||||||
|
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
(* 패턴 *)
|
(* 패턴 *)
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
pattern = "_" | literal | ctor_pattern | ident ;
|
pattern = "_" | literal | name_pattern ;
|
||||||
ctor_pattern = ident , "(" , pattern , { "," , pattern } , [ "," ] , ")" ;
|
|
||||||
|
(* 이름 하나로는 바인딩인지 생성자인지 갈리지 않는다. 판정 규칙:
|
||||||
|
* 한정되었으면(Shapes.Dot) 언제나 생성자다
|
||||||
|
* 괄호가 붙으면 생성자다
|
||||||
|
* 둘 다 아니면 바인딩이거나, 이름 해소가 생성자로 판정한다 *)
|
||||||
|
name_pattern = ident , [ "." , ident ] , [ "(" , [ list<pattern> ] , ")" ] ;
|
||||||
(* 가드 없음. 중첩은 제한 없음 (exhaustiveness 알고리즘이 처리한다) *)
|
(* 가드 없음. 중첩은 제한 없음 (exhaustiveness 알고리즘이 처리한다) *)
|
||||||
|
|||||||
+257
-6
@@ -8,6 +8,11 @@ AI의 코드 생성 속도 >> 신뢰 확보 속도.
|
|||||||
1. 오류는 더 일찍: 컴파일 타임으로 최대한 끌어당김
|
1. 오류는 더 일찍: 컴파일 타임으로 최대한 끌어당김
|
||||||
→ null 없음(Option), Result, exhaustive matching, definite init,
|
→ null 없음(Option), Result, exhaustive matching, definite init,
|
||||||
미선언 effect = compile error
|
미선언 effect = compile error
|
||||||
|
※ definite init은 문법이 이미 보장한다: let은 항상 초기화식을 요구하고
|
||||||
|
미초기화 바인딩을 쓸 방법이 없다. 별도 검사가 필요 없는 것이 맞다
|
||||||
|
※ exhaustive matching은 interface hash가 enum 정의 본문을 입력으로 삼는
|
||||||
|
이유이기도 하다. upstream에 variant가 하나 늘면 downstream의 match가
|
||||||
|
깨져야 하는데, 이 검사가 없으면 깨질 것이 없다
|
||||||
2. 검증은 더 빨리: 검증 속도가 언어 설계의 헌법
|
2. 검증은 더 빨리: 검증 속도가 언어 설계의 헌법
|
||||||
→ fast path(check) / slow path(release, deep verify) 분리
|
→ fast path(check) / slow path(release, deep verify) 분리
|
||||||
→ 컴파일을 느리게/비결정적으로 만드는 기능 원천 배제
|
→ 컴파일을 느리게/비결정적으로 만드는 기능 원천 배제
|
||||||
@@ -23,6 +28,16 @@ AI의 코드 생성 속도 >> 신뢰 확보 속도.
|
|||||||
→ "Effects: +payment.refund" 수준으로 리뷰 압축
|
→ "Effects: +payment.refund" 수준으로 리뷰 압축
|
||||||
5. 한 개념 = 한 방식: syntax variant 최소화, 공식 formatter 하나,
|
5. 한 개념 = 한 방식: syntax variant 최소화, 공식 formatter 하나,
|
||||||
공식 toolchain 하나. 단순성의 기준은 "작성자"가 아니라 "리뷰어와 검증기"
|
공식 toolchain 하나. 단순성의 기준은 "작성자"가 아니라 "리뷰어와 검증기"
|
||||||
|
6. 이름은 관례가 아니라 뜻에서 고른다 — 낯섦은 한 번 치르고 끝나지만
|
||||||
|
부정확함은 읽는 사람마다 매번 치른다
|
||||||
|
→ 판정: 그 단어로 그것이 무엇인지 평범한 문장을 써 보라. 단어가 문장을
|
||||||
|
도우면 맞는 이름, 문장과 싸우면 틀린 이름이다
|
||||||
|
"크래시는 복구하는 것이 아니라 조사하는 것이다" — 돕는다
|
||||||
|
"패닉은 복구할 수 없다" — 다른 언어에서는 할 수 있어 싸운다
|
||||||
|
→ 널리 쓰인다는 것은 옳다는 증거가 아니다. 운영체제의 "fault"가 그렇다 —
|
||||||
|
잘못이 아닌 것을 잘못이라 부르고, 그 오류까지 함께 전파된다
|
||||||
|
※ 관례가 정확하면 관례를 따른다. 관례라서가 아니라 정확해서다
|
||||||
|
※ 이름에만 적용된다. 동작을 관례와 다르게 만들 이유는 아니다
|
||||||
|
|
||||||
■ 파생 결정
|
■ 파생 결정
|
||||||
|
|
||||||
@@ -42,9 +57,22 @@ Alias/Move 모델 (철학 1,3에서 파생 — 언어 전체의 토대):
|
|||||||
"소비 여부를 타입에 드러내라"는 압력이라 리뷰 가능성에는 순기능이고,
|
"소비 여부를 타입에 드러내라"는 압력이라 리뷰 가능성에는 순기능이고,
|
||||||
실제로 고통스러우면 v1에서 Option::take류 표준 관용구로 흡수한다
|
실제로 고통스러우면 v1에서 Option::take류 표준 관용구로 흡수한다
|
||||||
- v0의 자원·capability는 affine이다: 중복 사용은 compile error,
|
- v0의 자원·capability는 affine이다: 중복 사용은 compile error,
|
||||||
미해제는 검사하지 않는다 (오용 금지, 누수 허용).
|
미해제는 타입 층에서 검사하지 않는다 (오용 금지, 누수 허용).
|
||||||
linear 검사와 해제 보장은 v1 과제
|
linear 검사와 해제 보장은 v1 과제.
|
||||||
|
※ 다만 lint가 조기 탈출(?, return) 자리에서 새는 것을 말한다. 막지 않고
|
||||||
|
말한다 — 언어가 강제할지는 with가 결정할 문제이고 그 결정에는 "실제로
|
||||||
|
얼마나 새는가"라는 측정이 필요하다. 저장소 전체에서 네 곳, 오탐 0건
|
||||||
|
- v0에 부분 move는 없다. affine 값이 합성 구조를 드나드는 모든 자리가 Move
|
||||||
|
자리다: struct 필드, 열거형 생성자, 열거형 분해, 리스트·struct 리터럴,
|
||||||
|
함수 인자, 반환, 클로저 capture. 앞의 셋이 뚫려 있었고 개밥 먹기가 찾았다
|
||||||
- v0에 first-class reference는 없다. mutable 데이터는 소유 변수를 통해서만 변경
|
- v0에 first-class reference는 없다. mutable 데이터는 소유 변수를 통해서만 변경
|
||||||
|
- 클로저는 mut 바인딩을 capture할 수 없다. 참조가 없으므로 별칭을 만들 수도,
|
||||||
|
조용히 복사할 수도 없기 때문이다. spawn 클로저 제한은 이 일반 규칙의 특수 사례다
|
||||||
|
- v0에 부분 move는 없다. 필드 접근은 빌림이고 결과도 빌린 값이다
|
||||||
|
※ struct에서 affine 필드만 꺼내 가려면 부분 move 상태 추적이 필요한데,
|
||||||
|
그 복잡도는 v0가 사려는 것이 아니다. 필요하면 통째로 own으로 받는다
|
||||||
|
- 외부 타입은 affine임을 증명할 수 없으므로 copyable로 본다.
|
||||||
|
모르는 것을 위반이라고 말하지 않는다 — 모듈 로딩이 생기면 판정된다
|
||||||
- 공유는 deep immutable 값만 가능 (내부 가변성 타입은 v0에 없음)
|
- 공유는 deep immutable 값만 가능 (내부 가변성 타입은 v0에 없음)
|
||||||
근거: "safe code에 data race 없음"은 spawn만 막아서 성립하지 않는다. closure,
|
근거: "safe code에 data race 없음"은 spawn만 막아서 성립하지 않는다. closure,
|
||||||
channel, container, 인자/반환 전 경로에서 mutable alias가 없어야 하며, 값 의미론
|
channel, container, 인자/반환 전 경로에서 mutable alias가 없어야 하며, 값 의미론
|
||||||
@@ -107,8 +135,8 @@ Affinity 전이 (보안 주장의 필수 전제):
|
|||||||
- structured concurrency만 허용: 태스크 수명 = 블록 구조 (locality)
|
- structured concurrency만 허용: 태스크 수명 = 블록 구조 (locality)
|
||||||
- 데이터 경쟁은 격리로: mutable은 단일 소유, channel로 소유권 이동,
|
- 데이터 경쟁은 격리로: mutable은 단일 소유, channel로 소유권 이동,
|
||||||
공유는 deep immutable만 (borrow checker는 complexity budget 초과)
|
공유는 deep immutable만 (borrow checker는 complexity budget 초과)
|
||||||
- spawn closure는 by-move capture 또는 immutable capture만 허용.
|
- spawn closure는 by-move capture 또는 immutable capture만 허용
|
||||||
mutable 참조 capture는 문법적으로 금지
|
(Alias/Move 모델의 mut capture 금지가 그대로 적용된다)
|
||||||
- spawn은 primitive가 아니라 TaskScope capability의 메서드다.
|
- spawn은 primitive가 아니라 TaskScope capability의 메서드다.
|
||||||
effect spawn은 아래 "정적/동적 층 분리"대로 그 타입에 묶인다
|
effect spawn은 아래 "정적/동적 층 분리"대로 그 타입에 묶인다
|
||||||
(PaymentGateway.refund와 동형)
|
(PaymentGateway.refund와 동형)
|
||||||
@@ -268,12 +296,23 @@ Generics (철학 2에서 파생):
|
|||||||
※ 각 단계는 그 단계가 소유한 성질만 판정한다. 예: affinity는 타입 동등성이
|
※ 각 단계는 그 단계가 소유한 성질만 판정한다. 예: affinity는 타입 동등성이
|
||||||
아니라 substructural 성질이므로 타입 검사가 아니라 move 검사가 소유한다.
|
아니라 substructural 성질이므로 타입 검사가 아니라 move 검사가 소유한다.
|
||||||
단계가 서로의 결론을 앞지르면 진단이 엉뚱한 곳에서 난다
|
단계가 서로의 결론을 앞지르면 진단이 엉뚱한 곳에서 난다
|
||||||
|
※ 다만 effect 검사는 타입 검사와 같은 순회에서 돈다. effect 변수의 해소가
|
||||||
|
타입 변수와 같은 지점(호출 지점의 지역 unification)에서 일어나므로,
|
||||||
|
떼어내면 순회와 인스턴스화를 두 번 하게 된다. 소유는 나뉘되 순회는 하나다
|
||||||
|
※ v0는 과잉 선언(선언했으나 수행하지 않는 effect)을 오류로 보지 않는다.
|
||||||
|
외부 모듈의 effect를 모르는 상태에서는 판정할 수 없기 때문이다.
|
||||||
|
모듈 로딩이 생기면 lint 대상이다
|
||||||
- move/affinity 검사, capability use 규칙, affinity 전이
|
- move/affinity 검사, capability use 규칙, affinity 전이
|
||||||
- effect 변수 (effect 다형성)
|
- effect 변수 (effect 다형성)
|
||||||
- interface artifact + hash 기반 incremental invalidation
|
- interface artifact + hash 기반 incremental invalidation
|
||||||
- cool check
|
- coolc check
|
||||||
- 얇은 typed IR + tree-walking interpreter (cool run 대용)
|
- 얇은 typed IR + tree-walking interpreter (coolc run)
|
||||||
※ IR을 미루면 non-IR 전제가 스며들어 재작성 됨. 지금, 얇게.
|
※ IR을 미루면 non-IR 전제가 스며들어 재작성 됨. 지금, 얇게.
|
||||||
|
※ 실행 시점에도 권한의 출처는 런타임 하나다. main이 선언한 capability만
|
||||||
|
넘어가고, 소스에는 capability를 만드는 문법이 없다 — 보안 정리 (i)의
|
||||||
|
실행 시점 대응물. TaskScope의 뿌리도 같은 이유로 런타임이 준다.
|
||||||
|
※ v0의 scope 실행 의미는 순차다. 구조가 먼저고 병렬성은 그 위의 최적화다 —
|
||||||
|
순서가 반대면 취소와 전파를 나중에 끼워 넣게 된다.
|
||||||
※ effect check가 fast path 예산 안에 드는지가 사활 → 최우선 검증 대상
|
※ effect check가 fast path 예산 안에 드는지가 사활 → 최우선 검증 대상
|
||||||
|
|
||||||
제외 (아키텍처 검증 후 얹어도 되는 것):
|
제외 (아키텍처 검증 후 얹어도 되는 것):
|
||||||
@@ -314,3 +353,215 @@ L2 빠른 테스트 / L3 fuzzing / L4 formal proof → 요청 시, 분리 실행
|
|||||||
오류를 더 빨리 잡는가? / 컴파일 복잡도·시간은 예측 가능한가? /
|
오류를 더 빨리 잡는가? / 컴파일 복잡도·시간은 예측 가능한가? /
|
||||||
invalidation 범위를 넓히는가? / unrelated code 의미를 바꾸는가? /
|
invalidation 범위를 넓히는가? / unrelated code 의미를 바꾸는가? /
|
||||||
기존 개념의 중복 표현인가? → 강한 이유 없으면 거절
|
기존 개념의 중복 표현인가? → 강한 이유 없으면 거절
|
||||||
|
|
||||||
|
■ 실패의 세 층 — 실패 / 결함 / 감독
|
||||||
|
서로 다른 세 질문에 답한다. 아래 층은 위 층 없이도 성립한다.
|
||||||
|
|
||||||
|
실패 (Result) 호출자가 대처할 수 있는가? 타입이 강제한다
|
||||||
|
결함 (crash) 이 태스크가 계속할 수 있는가? 컴파일러는 추적하지 않는다
|
||||||
|
감독 죽은 태스크를 어떻게 다루는가? capability가 강제한다
|
||||||
|
|
||||||
|
세 층이 세 가지 다른 기제로 규율된다는 것이 제대로 나뉘었다는 증거다.
|
||||||
|
"상황이 나쁨"은 결함이 아니라 실패다 — 디스크가 가득 찬 것은 Result다.
|
||||||
|
이 선을 긋지 않으면 crash가 게으름의 배출구가 된다.
|
||||||
|
※ 취소는 이 셋 중 어디에도 없다. 취소는 실패가 아니라 "더 이상 필요 없음"
|
||||||
|
이며 별도의 결정으로 남는다.
|
||||||
|
"오류"는 컴파일러가 내는 진단에만 쓴다.
|
||||||
|
|
||||||
|
■ 결함 — crash
|
||||||
|
crash(message) -> Never. 키워드다 (prelude가 없어 함수로 두면 매번 import).
|
||||||
|
- effect가 아니다. 경계 검사 하나에 {Crash}가 호출자 전부로 전염되면
|
||||||
|
effect 절은 신호가 아니라 잡음이 된다. 발산이 effect가 아닌 것과 같은
|
||||||
|
이유다 — 무한 루프도 추적하지 않는다.
|
||||||
|
- Never는 어떤 타입 자리에도 놓인다. 그래야 match 팔에서 쓸 수 있고,
|
||||||
|
그게 없으면 crash는 식 자리에서 못 쓴다.
|
||||||
|
- 언어 수준 recover가 없다. 붙잡는 것이 있으면 그것은 예외이고, 예외는
|
||||||
|
시그니처에 안 적히므로 철학 1과 충돌한다.
|
||||||
|
- 되감기를 하지 않는다. ※ crash 시 자원 해제 여부는 자원 모델과 함께
|
||||||
|
결정한다 — 지금은 열어둔다.
|
||||||
|
- 0으로 나누기, assert 실패, 미래의 범위·오버플로가 전부 이 하나로 모인다.
|
||||||
|
※ crash는 프로그램의 결함을 말한다. 예상되는 실패는 Result다. 호출자가
|
||||||
|
대처할 수 있는 것을 crash로 처리하면 오용이다.
|
||||||
|
|
||||||
|
■ crash는 어디까지 올라가는가 (실패의 단위)
|
||||||
|
콜 스택의 root가 아니라 scope 트리의 root다. 즉 실패의 단위는 프로세스가
|
||||||
|
아니라 태스크다. scope가 이미 그 모양이기 때문이다 — 렉시컬이고, 블록을
|
||||||
|
나가는 것이 join이며, 자식이 죽었다는 사실이 부모에게 도달하는 지점이
|
||||||
|
문법에 이미 있다(닫는 중괄호).
|
||||||
|
|
||||||
|
crash는 scope 트리를 타고 올라간다. 자식이 crash하면 그 scope는 join
|
||||||
|
지점에서 crash한다. root까지 도달하면 실행이 끝난다.
|
||||||
|
|
||||||
|
v0는 태스크가 하나뿐이라 이 규칙이 관측되지 않는다 — "태스크가 죽는다"와
|
||||||
|
"프로그램이 죽는다"가 같은 사건이다. 그래도 지금 적는 이유는, 동시성이
|
||||||
|
진짜가 될 때 이 결정을 새로 하면 이미 쓰인 코드의 의미가 바뀌기 때문이다.
|
||||||
|
|
||||||
|
전파는 취소를 정하지 않고도 정의된다. 취소가 생기면 형제들이 언제 멈추는지가
|
||||||
|
바뀔 뿐, scope가 실패한다는 사실은 바뀌지 않는다.
|
||||||
|
※ 형제 취소는 여전히 미정이다. 대가를 알고 미룬다 — 취소가 없으면 형제
|
||||||
|
하나가 끝나지 않을 때 죽은 자식의 crash가 join에 도달하지 못한다.
|
||||||
|
실패가 hang에 가려진다. 이것이 취소가 필요한 이유이기도 하다.
|
||||||
|
|
||||||
|
■ 얼랭 방식은 되는가 — 감독은 되고 비구조적 spawn은 안 된다
|
||||||
|
공유 가변 상태 없음, let it crash, 죽은 쪽의 상태를 물려받지 않음 —
|
||||||
|
얼랭과 이미 같다. 다른 경로로 같은 결론에 도착했다.
|
||||||
|
가져오지 않는 것: 얼랭의 비구조적 spawn(Pid가 부모보다 오래 산다).
|
||||||
|
부모 없는 태스크는 ambient authority와 같은 문제이고, 구조적 동시성을 고른
|
||||||
|
이유가 그것이다.
|
||||||
|
가져오는 것: 감독. scope 트리가 이미 supervision 트리 모양이다.
|
||||||
|
sc.spawn(f) 실패가 부모로 전파된다
|
||||||
|
sup.spawn(sc, f) 실패가 여기서 멈춘다
|
||||||
|
spawn은 하나의 개념이고 실패가 어디까지 가느냐가 권한으로 갈린다 — 실패를
|
||||||
|
삼키려면 Supervisor를 받았어야 하고 그것이 시그니처에 보인다. 문법 변경 없음.
|
||||||
|
재시작 가능한 자식은 copyable 클로저여야 한다(fn vs affine fn). 얼랭이 관례로
|
||||||
|
지키는 것을 affinity가 이미 검사한다. 재시작 전략은 라이브러리다(얼랭도 OTP).
|
||||||
|
※ 전제조건은 자원 모델이다. 죽은 태스크가 잡았던 것이 정리되지 않으면
|
||||||
|
재시작이 곧 누수다.
|
||||||
|
※ 얼랭의 보장은 BEAM의 선점 스케줄링과 프로세스별 힙 위에 선다. 우리에겐
|
||||||
|
아직 둘 다 없다 — 이것은 설계 층의 답이지 v0가 보여줄 수 있는 것이 아니다.
|
||||||
|
|
||||||
|
■ 격리 경계는 recover가 아니다
|
||||||
|
런타임은 격리 경계를 가질 수 있다. 테스트 러너가 첫 사례이고, 서버의
|
||||||
|
요청 경계가 두 번째가 될 것이다. 구분선은 이것이다:
|
||||||
|
|
||||||
|
실패한 계산이 만든 값은 경계를 넘지 못한다.
|
||||||
|
경계가 얻는 것은 "죽었다"는 사실과 메시지뿐이다.
|
||||||
|
|
||||||
|
recover는 같은 스택에서 재개하고 지역 변수에 접근한다 — 금지다.
|
||||||
|
격리 경계는 죽었음을 관찰할 뿐이다 — 허용이다. Erlang의 supervisor가 죽은
|
||||||
|
프로세스의 상태를 물려받지 않는 것과 같다.
|
||||||
|
테스트 러너가 이 원칙대로다: 실패한 테스트에서 아무 값도 가져오지 않고
|
||||||
|
이름과 메시지만 얻는다. 서버 경계도 언어 기능이 아니라 scope 위에 얹는다.
|
||||||
|
|
||||||
|
■ 내장 테스트
|
||||||
|
test "이름" { ... }. 파라미터가 없으므로 capability를 받을 수 없고,
|
||||||
|
capability를 만드는 문법도 없다. 따라서 effect-free임이 증명된다 —
|
||||||
|
관례가 아니라 검사다. 그 결과:
|
||||||
|
- 파일도 시계도 못 건드린다. 같은 입력이면 같은 결과다
|
||||||
|
- 순서에 의존하지 않고 병렬로 돌려도 같다 → 결과를 캐시할 수 있다
|
||||||
|
(인터페이스 해시가 안 변하면 재검사하지 않는 것과 같은 논리)
|
||||||
|
- 자원을 가질 수 없다 (획득에 effect가 필요하므로). 그래서 "테스트가
|
||||||
|
죽으면 자원은?"이라는 질문이 애초에 생기지 않는다
|
||||||
|
interface hash에 들어가지 않는다 — 테스트를 고쳤다고 downstream이
|
||||||
|
재검사되면 안 된다.
|
||||||
|
컴파일 타임 메타프로그래밍 없음. assert는 std/test.cool에 coollang으로
|
||||||
|
쓰인다 (crash 위의 설탕) — std에서 본문이 있는 첫 함수다. 실패 메시지에
|
||||||
|
값이 안 나오는 것은 의도다. 표현식 텍스트를 잡으려면 매크로가 필요하다.
|
||||||
|
※ effect 있는 코드는 테스트할 수 없다. 가짜 capability를 만드는 수단이
|
||||||
|
없기 때문이다. 실제로 불편해진 뒤에 판단한다.
|
||||||
|
|
||||||
|
■ 버려지는 값
|
||||||
|
꼬리가 아닌 자리의 식이 값을 남기면 오류다. 남긴 값은 버려지는데, 그 값이
|
||||||
|
Result면 실패가 조용히 사라진다 — 철학 1과 정면으로 부딪힌다.
|
||||||
|
일부러 버리려면 let _ = 로 적는다. 버린다는 사실이 코드에 보여야 한다.
|
||||||
|
※ 개밥 먹기 2에서 발견했다. 그전까지 "이 언어에는 실패를 버릴 방법이 없다"고
|
||||||
|
문서와 투어에 적혀 있었는데 틀린 말이었다 — List.each에 Result 반환 클로저를
|
||||||
|
못 넘긴다는 좁은 사실을 언어 전체의 성질로 일반화한 것이었다.
|
||||||
|
부산물로 정리 경로의 관용구가 생겼다: let _ = fs.close(f).
|
||||||
|
|
||||||
|
■ lint (오류다, 경고가 아니다)
|
||||||
|
- 미사용 import: 쓰지 않는 모듈의 시그니처가 바뀌면 이 모듈이 재검사된다.
|
||||||
|
증분 루프의 비용을 이유 없이 넓히는 선언은 남겨둘 수 없다.
|
||||||
|
- effect 과잉 선언: 선언하고 수행하지 않으면 호출자가 하지도 않는 일에
|
||||||
|
의무를 진다. 시그니처는 실제보다 좁아도 안 되고 넓어도 안 된다.
|
||||||
|
단, effect 변수가 있거나 본문에 모르는 이름이 있으면 판정하지 않는다 —
|
||||||
|
무엇이 묶일지는 호출 지점이 정하고, 외부 타입의 메서드는 effect를 모른다.
|
||||||
|
lint는 blocking이 아니다. 이름 해소 실패는 뒤 단계를 막지만 lint는 막지
|
||||||
|
않는다 — lint 하나가 진짜 타입 오류를 가리면 루프가 느려진다.
|
||||||
|
|
||||||
|
■ 파서 오류 복구
|
||||||
|
항목 단위로만 회복한다. 오류가 난 선언은 통째로 버리고 다음 선언에서
|
||||||
|
다시 시작한다. 문 단위로 더 잘게 회복하려 하면 파서가 추측을 하게 되고,
|
||||||
|
틀린 추측은 없는 오류를 지어낸다. 한 항목에 오류 하나가 상한이라는 것은
|
||||||
|
정직한 한계이지 숨길 것이 아니다.
|
||||||
|
동기화 지점: 중괄호 깊이 0 + 줄 첫머리 + 선언 시작 토큰. 셋 다 필요하다.
|
||||||
|
|
||||||
|
■ 표준 라이브러리 (std/)
|
||||||
|
본문 없는 선언 파일이다. 런타임이 구현하고 .cool 파일은 계약만 말한다.
|
||||||
|
구현이 아니라 시험대인 것이 요점 — effect 다형성이 실제로 쓸 만한지가
|
||||||
|
List.each와 List.map에서 결정된다. 규칙이 틀렸으면 여기서 드러난다.
|
||||||
|
prelude는 없다. std도 명시적으로 가져온다 — 암묵적으로 끌어오지 않는다는
|
||||||
|
규칙에 예외를 두지 않는다.
|
||||||
|
※ std를 넣자마자 샘플 01이 깨졌다. List.each에 Result를 반환하는 클로저를
|
||||||
|
넘기고 그 안에서 ?를 쓰고 있었다. 검사되지 않던 코드가 검사되기 시작한
|
||||||
|
것이고, 이것이 std를 "부채 상환"이 아니라 "검증"으로 본 이유다.
|
||||||
|
결과를 버릴 방법이 언어에 없다는 성질도 여기서 처음 확인됐다.
|
||||||
|
|
||||||
|
■ 개밥 먹기 2 — dogfoods/ (2026-08)
|
||||||
|
프로그램을 쓰되 돌리지 않는다. std를 선언만 두고 그것에 대고 실제 유스케이스를
|
||||||
|
쓴다. std가 원래 선언-전용이므로 실행만 빼고 전부 진짜로 검사된다 — 종이
|
||||||
|
스케치가 아니라 컴파일러가 검증한 설계다.
|
||||||
|
합격 기준이 둘이다: check exit 0 + 모듈이 실제로 해소될 것. 후자가 없으면
|
||||||
|
전자가 공허하다 — import가 조용히 해소되지 않으면 이름이 전부 불투명해져
|
||||||
|
검사기가 무엇이든 통과시킨다. 첫 시도에서 실제로 그 일이 일어났다.
|
||||||
|
2번(Atomic File Updater) 결과: ?를 자원과 함께 쓸 수 없다. 조기 반환이 정리를
|
||||||
|
건너뛰는데 검사기는 통과시킨다(v0 정책: 오용 금지, 누수 허용). 제대로 정리하면
|
||||||
|
18줄이 58줄이 되고 5단 중첩 match가 된다. resource/with가 필요한 이유가 여기
|
||||||
|
숫자로 있다.
|
||||||
|
6번(DB Pool)까지 두 점을 모아 resource/with를 넣기로 결정했다. 근거:
|
||||||
|
자원을 들면 ?를 한 번도 못 쓴다(2/2), 같은 정리 패턴을 각자 발명했다(2/2),
|
||||||
|
누수가 조용히 통과한다(2/2), 2.3~3.2배.
|
||||||
|
고통은 자원 개수가 아니라 정리의 균일성에 비례한다 — 파일은 단계마다 정리가
|
||||||
|
달라 4단 중첩이 되고, 풀은 언제나 반납이라 1단으로 접힌다.
|
||||||
|
순서가 정해졌다: 먼저 수신자를 소비하는 메서드(자원이 자기 정리를 스스로
|
||||||
|
선언할 수 있어야 한다), 그다음 resource/with. release(pool, own lease)처럼
|
||||||
|
정리에 다른 값이 필요하면 with가 자동으로 부를 수 없기 때문이다.
|
||||||
|
앞의 것을 넣었다 — own self. capability 메서드의 첫 자리에만 오고, 빌리는
|
||||||
|
것이 기본이므로 빌림에는 적을 말이 없다. 정리에 자원 자신만 있으면 되게
|
||||||
|
됐고, 부수로 권한 약화가 공짜로 생겼다: 핸들만 넘기면 그 파일에 쓸 권한만
|
||||||
|
넘어가고 파일 시스템 전체 권한은 안 넘어간다.
|
||||||
|
고침: 함수 타입과 클로저 파라미터의 own. 그 구멍이 std의 fold가 틀린 것을
|
||||||
|
덮고 있었다 — 누적자는 매 단계 소비되므로 own이다.
|
||||||
|
LRU 캐시가 제약 하나를 더했다: 축출은 렉시컬이 아니므로 with가 못 닿는다.
|
||||||
|
따라서 자원은 렉시컬 전용으로 간다 — 자원을 담는 컨테이너(풀, 캐시,
|
||||||
|
레지스트리)는 런타임만 만들 수 있고, 사용자는 언어로 못 만든다. 그 대가를
|
||||||
|
알고 택한다. 이미 그렇게 되어 있었다 — std-draft의 Pool이 불투명한 런타임
|
||||||
|
capability인 이유가 만들 수 없어서였다.
|
||||||
|
그리고 큰 버그 하나를 잡았다: Option/Result의 패턴이 타입을 통째로 잃고
|
||||||
|
있었다. 내장 생성자가 typecheck에 등록되지 않아 None이 변수 바인딩이 되고
|
||||||
|
Ok(v)의 v가 TUnknown이었다. Ok(v) => v 로 Int를 String 자리에 두는 코드가
|
||||||
|
통과했다. 소진성 검사는 자기 목록에 내장을 갖고 있어 이 사실을 덮고 있었다.
|
||||||
|
Mini Shell(1번)이 네 번째이자 다른 모양의 점을 줬다. 앞의 셋은 한 자원이
|
||||||
|
한 곳으로 갔다. 파이프는 끝이 둘이고 각각 다른 자식에게 간다 — 부모에게는
|
||||||
|
아무것도 안 남고 어느 블록에도 묶이지 않는다. 가장 어려운 자원 코드가
|
||||||
|
렉시컬하지 않다.
|
||||||
|
그래서 with를 미룬다. resource라는 새 종류는 넣지 않기로 확정했다 —
|
||||||
|
capability가 이미 그것이고 own self가 정리를 말하므로, 종류를 더 만들면
|
||||||
|
95%가 같은 둘이 생긴다. with는 두 갈래(자동 반납 / 소비 강제) 중 하나를
|
||||||
|
고르는 문제이고, 렉시컬 도구를 먼저 넣으면 그것이 닿지 않는 자리를 나중에
|
||||||
|
다시 설계해야 한다. 다시 여는 조건: Process Supervisor나 Reverse Proxy에서
|
||||||
|
렉시컬한 자원 코드가 다수면 고른다.
|
||||||
|
Mini Shell이 보안 구멍 셋을 더 찾았다. 열거형 생성자가 인자를 소비하지
|
||||||
|
않았고(Some(f)를 두 번), 소유한 열거형을 두 번 분해할 수 있었고(같은
|
||||||
|
파이프에서 같은 fd를 두 번), 점 왼쪽을 이름 해소가 검사하지 않아 가져오지도
|
||||||
|
않은 이름이 조용히 통과했다(Nonsense.whatever). 셋 다 D1·D11과 같은 병이다 —
|
||||||
|
규칙은 있었는데 그 규칙이 닿아야 할 자리 하나가 비어 있었다.
|
||||||
|
그리고 개밥이 처음으로 실제로 돌았다. 파싱을 자원에서 떼어 놓았더니
|
||||||
|
mini-shell/parse.cool의 내장 test 5개가 통과한다.
|
||||||
|
전문은 dogfoods/FINDINGS.md.
|
||||||
|
|
||||||
|
■ 개밥 먹기 1 — samples/app (2026-08)
|
||||||
|
samples/app — 설정 파서 + 리포트 도구, 2모듈 232줄. 검사기를 시험하려고
|
||||||
|
쓴 것이 아니라 일을 하려고 쓴 첫 프로그램이다.
|
||||||
|
결과: 되돌리기 비싼 결정은 하나도 후회되지 않았고(capability 전달, effect
|
||||||
|
명시, 실패를 버릴 수 없음, 소진적 match), 불편은 전부 되돌리기 싼 것들이었다
|
||||||
|
— 리스트 n번째 접근 없음, String.concat 2항. "else if가 없다"고 적은
|
||||||
|
항목 하나는 확인해 보니 관찰자가 틀린 것이었다 — 개밥 먹기의 불편은
|
||||||
|
언어의 성질일 수도, 쓴 사람의 습관일 수도 있다.
|
||||||
|
전문과 증거는 docs/friction.md. 이것이 v1 설계의 첫 입력이다.
|
||||||
|
후속: std 보강(List.first/nth/enumerate, String.join, Option/Result 함수)으로
|
||||||
|
292줄이 232줄이 됐다. 문법은 하나도 건드리지 않았다 — 걸린 것이 전부 std의
|
||||||
|
빈 곳이었지 문법의 문제가 아니었다는 뜻이고, 그것 자체가 결과다.
|
||||||
|
마찰 8건 중 2건은 "언어가 못 한다"고 적었다가 확인해 보니 되는 것이었다
|
||||||
|
(else if, enumerate). 개밥 먹기에는 확인 절차가 함께 있어야 한다.
|
||||||
|
|
||||||
|
■ 측정 (2026-08, v0 fast path)
|
||||||
|
100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현.
|
||||||
|
cold 전체 검사 245ms
|
||||||
|
본문만 수정 0.8ms, 재검사 1개 모듈
|
||||||
|
시그니처 수정 2.1ms, 재검사 2개 모듈 (+ downstream 소진성 위반 검출)
|
||||||
|
증분 루프의 비용은 시간이 아니라 재검사 범위가 결정한다. 본문 수정이
|
||||||
|
downstream을 한 칸도 건드리지 않는 것이 이 아키텍처의 주장이고, 위 수치의
|
||||||
|
"재검사 1개"가 그 주장이다. 시그니처 수정이 사슬 끝까지 가지 않고 2개에서
|
||||||
|
멈추는 것도 같은 규칙의 결과다 — m101의 interface는 변하지 않으므로 m102는
|
||||||
|
다시 볼 이유가 없다.
|
||||||
|
|||||||
@@ -0,0 +1,715 @@
|
|||||||
|
# 개밥 먹기 발견 기록
|
||||||
|
|
||||||
|
`dogfoods/`를 쓰면서 나온 것들. 고친 것과 열어둔 것을 같이 적는다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Atomic File Updater (2026-08-30)
|
||||||
|
|
||||||
|
목적: 동시성을 안 섞고 **자원 모델만** 시험한다. 임시 파일에 쓰고, 디스크에
|
||||||
|
내리고, 원자적으로 바꿔치기한다. 실패 경로가 넷이고 전부 정리가 필요하다.
|
||||||
|
|
||||||
|
산출물: `std-draft/fs.cool` 65줄, `atomic-update/naive.cool` 18줄,
|
||||||
|
`atomic-update/careful.cool` 58줄.
|
||||||
|
|
||||||
|
### D1. 상대 경로 import가 패키지로 오인됐다 — 고침
|
||||||
|
|
||||||
|
`import "../std-draft/fs"`가 조용히 해소되지 않았다. `is_package`가 첫
|
||||||
|
세그먼트에 점이 있는지만 봤는데 `..`이 걸렸다.
|
||||||
|
|
||||||
|
**무서운 것은 버그 자체가 아니라 그 결과다.** import가 해소되지 않으면 그
|
||||||
|
모듈의 이름이 전부 불투명해지고, "모르는 것을 틀렸다고 말하지 않는다"는 원칙에
|
||||||
|
따라 검사기가 무엇이든 통과시킨다. 첫 `coolc check` 결과가 exit 0이었는데
|
||||||
|
**아무 뜻도 없었다.** 없는 메서드를 불러도 통과했다.
|
||||||
|
|
||||||
|
고친 뒤 같은 코드가 정확히 잡힌다:
|
||||||
|
|
||||||
|
```
|
||||||
|
capability Fs.Fs에 this_does_not_exist 메서드가 없습니다
|
||||||
|
```
|
||||||
|
|
||||||
|
→ `dogfoods/README.md`의 합격 기준 ②가 여기서 나왔다.
|
||||||
|
|
||||||
|
### D2. 값 있는 식을 문으로 버릴 수 있었다 — 고침
|
||||||
|
|
||||||
|
```cool
|
||||||
|
fs.remove(path) // Result가 조용히 사라진다
|
||||||
|
1
|
||||||
|
```
|
||||||
|
|
||||||
|
이것이 통과했다. 즉 **`Result`를 버리는 방법이 있었다.** 개밥 먹기 1차
|
||||||
|
보고서와 언어 투어에서 내가 "이 언어에는 실패를 버릴 방법이 없다"고 적었는데
|
||||||
|
틀렸다 — `List.each`에 Result 반환 클로저를 못 넘긴다는 좁은 사실을 언어 전체의
|
||||||
|
성질로 일반화했다.
|
||||||
|
|
||||||
|
이제 꼬리가 아닌 자리의 식이 값을 남기면 오류다:
|
||||||
|
|
||||||
|
```
|
||||||
|
이 식이 남기는 Result[Unit, IoError]이(가) 버려집니다
|
||||||
|
(일부러 버리려면 let _ = 로 적으십시오)
|
||||||
|
```
|
||||||
|
|
||||||
|
부산물이 좋다. 정리 경로에서 오류를 **일부러** 무시하는 관용구가 생겼고,
|
||||||
|
버린다는 사실이 코드에 보인다:
|
||||||
|
|
||||||
|
```cool
|
||||||
|
let _ = fs.close(f) // 정리 중의 실패는 삼킨다 — 원래 오류가 더 중요하다
|
||||||
|
discard(fs, tmp, e)
|
||||||
|
```
|
||||||
|
|
||||||
|
### D3. `?`를 자원과 함께 쓸 수 없다 — 이번 개밥 먹기의 본체
|
||||||
|
|
||||||
|
`naive.cool` 18줄은 이렇게 생겼고 **통과한다**:
|
||||||
|
|
||||||
|
```cool
|
||||||
|
let f = fs.create(tmp)?
|
||||||
|
fs.write(f, contents)? // 실패하면 f가 안 닫히고 tmp가 남는다
|
||||||
|
fs.sync(f)?
|
||||||
|
fs.close(f)?
|
||||||
|
fs.rename(tmp, path)?
|
||||||
|
```
|
||||||
|
|
||||||
|
`?`가 조기 반환하므로 정리를 건너뛴다. v0가 "오용 금지, 누수 허용"이라
|
||||||
|
검사기는 아무 말도 하지 않는다.
|
||||||
|
|
||||||
|
제대로 정리하면 `careful.cool` 58줄이 되고 **5단 중첩 match**가 된다.
|
||||||
|
`?`를 한 번도 못 쓴다. 같은 일에 **3.2배**다.
|
||||||
|
|
||||||
|
이것이 `resource` / `with` 제안이 필요한 이유의 전부다. 그리고 그 제안이
|
||||||
|
암묵적 drop보다 나은 이유도 여기서 보인다 — 정리 지점이 닫는 중괄호로 눈에
|
||||||
|
보이고, `Fs.close`가 effects 절에 나타나 검사된다.
|
||||||
|
|
||||||
|
### D4. capability 메서드가 수신자를 소비할 수 없다 — 고침
|
||||||
|
|
||||||
|
`close`가 핸들을 소비해야 하는데 `own`은 파라미터에만 붙었다. capability
|
||||||
|
메서드에는 수신자를 적는 자리가 없었다.
|
||||||
|
|
||||||
|
처음에는 우회했다 — 핸들의 메서드가 아니라 `Fs`의 메서드로 두고 핸들을
|
||||||
|
인자로 받는 형태다. "읽기에 나쁘지 않고 오히려 권한이 필요하다는 게
|
||||||
|
보인다"고 적었는데, **D9이 그 판단을 뒤집었다.** 정리에 바깥 권한이
|
||||||
|
따라붙으면 `with`가 자원만 보고 무엇을 부를지 알 수 없다.
|
||||||
|
|
||||||
|
**넣은 것**: `own self`를 capability 메서드의 첫 파라미터로.
|
||||||
|
|
||||||
|
```cool
|
||||||
|
pub capability WriteFile {
|
||||||
|
fn write(s: String) effects {WriteFile.write} -> Result[Unit, IoError]
|
||||||
|
fn close(own self) effects {WriteFile.close} -> Result[Unit, IoError]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
빌리는 것이 기본이므로 **빌림에는 적을 말이 없다.** `self`만 적는 문장은
|
||||||
|
존재하지 않고, 그것을 쓰면 문법이 아니라 뜻으로 거절한다:
|
||||||
|
|
||||||
|
```
|
||||||
|
self는 own self로만 적습니다 (수신자를 빌리는 것이 기본이라 적을 말이 없습니다)
|
||||||
|
```
|
||||||
|
|
||||||
|
일반 `fn`에는 이 자리가 문법에 없다. 좌인수분해로 `"own"`을 먼저 먹고
|
||||||
|
그다음을 보므로 LL(1)이 유지된다 (진짜 충돌 0건).
|
||||||
|
|
||||||
|
**부수 발견 — 권한이 좁아진 채로 전달된다.** 1판에서는 파일에 쓰게 하려면
|
||||||
|
`Fs`를 통째로 줘야 했다. 이제 핸들만 넘기면 그 파일에 쓸 권한만 넘어간다.
|
||||||
|
정리를 자원 쪽으로 옮긴 것이 권한 약화(attenuation)를 공짜로 만들었다 —
|
||||||
|
설계할 때 노린 것이 아니라 나중에 보인 것이다.
|
||||||
|
|
||||||
|
**부수 발견 — 중첩 수신자에 구멍이 있었다.** move 검사의 `callee_info`가
|
||||||
|
수신자가 이름일 때만 시그니처를 찾았다. `h.lease.release()`는 못 찾아
|
||||||
|
조용히 빌림이 됐고, `own self`가 생기는 순간 그것은 자원을 두 번 소비할 수
|
||||||
|
있다는 뜻이 된다. 같은 커밋에서 막았다 (D11의 부분 move 검사로 떨어진다).
|
||||||
|
|
||||||
|
**부수 발견 — 진단이 부정확했다.** 메서드 이름만으로 아무 capability에서나
|
||||||
|
시그니처를 찾았기 때문에, 이름이 겹치면 엉뚱한 시그니처를 읽고 "빌린 값이라
|
||||||
|
소비할 수 없습니다"를 잘못 찍었다. 수신자의 선언된 타입을 알면 그 capability
|
||||||
|
안에서만 찾도록 고쳤다.
|
||||||
|
|
||||||
|
**2판으로 다시 쓴 결과** (같은 일을 하는 코드):
|
||||||
|
- `dogfoods/std-draft/fs.cool` — `WriteFile`이 메서드를 갖는다. 1판에서는 하나도 없었다
|
||||||
|
- `atomic-update/careful.cool` — `abandon`이 `fs`를 닫는 데 쓰지 않는다
|
||||||
|
- `db-pool/careful.cool` — `release_then(db, pool, l, r)` → `release_then(l, r)`
|
||||||
|
- `lru-cache/resources.cool` — `drain(db, pool, p)` → `drain(p)`
|
||||||
|
|
||||||
|
**도우미의 시그니처가 정리의 진짜 의존을 말하게 됐다.** 그것이 D9이 요구한
|
||||||
|
것이고, `with`의 전제 조건이다.
|
||||||
|
|
||||||
|
### D5. 함수 타입에 `own`이 없다 — 열림, 그리고 이건 구멍이다
|
||||||
|
|
||||||
|
```cool
|
||||||
|
pub fn apply(f: fn(own Handle) -> Handle, own h: Handle) -> Handle
|
||||||
|
^^^ 타입이(가) 필요합니다 — own 발견
|
||||||
|
```
|
||||||
|
|
||||||
|
문법이 `fn_type = "fn" "(" type_list ")"`라 `own`이 못 들어간다. 그래서
|
||||||
|
**소유권을 가져가는 클로저를 타입으로 표현할 수 없다.**
|
||||||
|
|
||||||
|
결과로 이것이 통과한다:
|
||||||
|
|
||||||
|
```cool
|
||||||
|
List.fold(xs, h, fn(acc, n) { step(acc, n) }) // step은 own을 받는다
|
||||||
|
```
|
||||||
|
|
||||||
|
`fold`의 시그니처는 `fn(acc, a) -> acc`이고 무표기는 빌림인데, 클로저
|
||||||
|
리터럴의 파라미터에는 소유권 표시가 없어 move 검사기가 소유한 값처럼
|
||||||
|
취급한다. **타입은 "빌린다"고 말하는데 리터럴은 "가져간다"처럼 행동한다.**
|
||||||
|
|
||||||
|
기본 검사는 멀쩡하다 — 빌린 값 옮기기도 이중 소비도 정확히 잡는다.
|
||||||
|
**고차 경계에서만 뚫린다.** Atomic Updater는 선형 코드라 안 걸렸지만,
|
||||||
|
Mini Shell(파이프라인을 따라 FD를 나름)과 DB Pool(lease를 fold로 다룸)은
|
||||||
|
정면으로 걸린다.
|
||||||
|
|
||||||
|
→ 그 둘을 쓰기 전에 결정해야 한다.
|
||||||
|
|
||||||
|
### D5 후속 — 고침, 그리고 std의 실수 하나가 딸려 나왔다
|
||||||
|
|
||||||
|
`own`을 함수 타입과 클로저 파라미터에 넣었다:
|
||||||
|
|
||||||
|
```cool
|
||||||
|
fn(own Handle) -> Handle // 타입에 적을 수 있다
|
||||||
|
List.fold(xs, h, fn(own acc, n) {...}) // 리터럴에도 적는다
|
||||||
|
```
|
||||||
|
|
||||||
|
클로저 파라미터의 소유권은 **리터럴이 스스로 적는다.** 타입은 기대 타입에서
|
||||||
|
읽어오지만 소유권은 읽어오지 않는다 — move 검사는 타입 검사와 별도 순회라
|
||||||
|
타입을 모르고, 소유권은 타입보다 결과가 크기 때문이다.
|
||||||
|
|
||||||
|
`unify`가 정확히 일치를 요구한다. 빌리는 클로저를 소유 자리에 넘기는 것은
|
||||||
|
안전하지만 그 반대는 아니고, 방향을 다루려면 부분 타입이 필요한데 없다.
|
||||||
|
|
||||||
|
**딸려 나온 것**: `std/list.cool`의 `fold`가 틀려 있었다.
|
||||||
|
|
||||||
|
```cool
|
||||||
|
f: fn(acc, a) -> acc // 전 — 빌림
|
||||||
|
f: fn(own acc, a) -> acc // 후 — 누적자는 매 단계 소비되고 새것으로 바뀐다
|
||||||
|
```
|
||||||
|
|
||||||
|
빌림으로 적혀 있어서 **affine 값을 fold로 실어나를 수 없었다.** 그런데 그
|
||||||
|
사실이 드러나지 않았던 이유가 바로 이 구멍이었다 — 클로저 파라미터를 무조건
|
||||||
|
소유로 봤으니 아무 오류도 안 났다. **구멍이 자기가 숨긴 버그를 덮고 있었다.**
|
||||||
|
|
||||||
|
#### 남은 한계 — 제네릭을 통과해 보지 못한다
|
||||||
|
|
||||||
|
move 검사는 타입이 없어 `fold`의 `acc`가 호출 지점에서 무엇으로 묶이는지
|
||||||
|
모른다. 그래서 클로저 파라미터에 표기가 없고 기대 타입이 제네릭 변수면
|
||||||
|
affinity를 판정하지 못한다. 지금은 소유권 표기 불일치로 잡히지만, 표기가
|
||||||
|
양쪽 다 없으면 통과한다.
|
||||||
|
|
||||||
|
근본 해법은 move 검사가 타입을 보는 것이고, 그건 두 순회를 합치는 일이다.
|
||||||
|
v0에서는 하지 않는다.
|
||||||
|
|
||||||
|
#### 대가 하나 — own이 흔해진다
|
||||||
|
|
||||||
|
`own`은 흔하지 않은 쪽에 붙는 표기인데, `fold`가 항상 요구하면 흔해진다.
|
||||||
|
`Config` 같은 copyable 누적자에도 `own`을 적게 된다. 정확히 일치를 요구한
|
||||||
|
결과이고, 부분 타입을 넣으면 사라진다. **표기의 신호가 약해지는지 지켜본다.**
|
||||||
|
|
||||||
|
### D6. 문자에 접근할 방법이 없다 — 열림
|
||||||
|
|
||||||
|
`String`에 `split`, `trim`, `starts_with`, `contains`뿐이다. 인덱싱도
|
||||||
|
`chars`도 없어 어휘 분석을 쓸 수 없다. Atomic Updater에는 파싱이 없어 안
|
||||||
|
걸렸지만 Mini Shell은 여기서 막힌다.
|
||||||
|
|
||||||
|
되돌리기 싼 std 문제다.
|
||||||
|
|
||||||
|
### D7. effect-free 테스트로는 이 프로그램을 하나도 테스트할 수 없다 — 예상됨
|
||||||
|
|
||||||
|
`update`는 전부 effect다. 테스트는 capability를 받지 않으므로 부를 수 없다.
|
||||||
|
설계대로이고, 대가가 이제 실물로 보인다.
|
||||||
|
|
||||||
|
가짜 capability를 만들 수단이 없는 한 이 층은 테스트 밖에 있다.
|
||||||
|
※ 가짜 capability를 허용해도 보안 성질은 안 깨질 것으로 보인다 — 클로저로
|
||||||
|
만든 가짜 `Fs`는 진짜 권한을 갖지 않는다. 실제로 불편해진 뒤에 판단한다.
|
||||||
|
|
||||||
|
### 소유권 검사가 실제로 잡는 것 (확인)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| 두 번 닫기 | **잡힘** — `f은(는) 이미 move되었습니다 (4:22에서 소비)` |
|
||||||
|
| 닫은 뒤 쓰기 | **잡힘** — 같은 진단 |
|
||||||
|
| 빌린 핸들을 반환 | **잡힘** — `빌린 값이라 반환할 수 없습니다` |
|
||||||
|
| 소유한 핸들을 반환 | 통과 — 옳다. `create`가 그렇게 생겼다 |
|
||||||
|
| 조기 반환으로 누수 | **통과** — v0 정책("오용 금지, 누수 허용") |
|
||||||
|
|
||||||
|
마지막 줄이 D3이고, `with`가 닫으려는 자리다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. DB Connection Pool (2026-08-30)
|
||||||
|
|
||||||
|
목적: 자원 모델의 **두 번째 데이터 점**. 파일에 없던 셋을 압박한다 —
|
||||||
|
자원이 두 층(풀/lease), 반납이 파괴가 아님, 획득이 실패할 수 있음.
|
||||||
|
|
||||||
|
산출물: `std-draft/db.cool` 50줄, `db-pool/naive.cool` 25줄,
|
||||||
|
`db-pool/careful.cool` 58줄.
|
||||||
|
|
||||||
|
### D8. 인자 안에서 소비와 사용을 섞을 수 없다
|
||||||
|
|
||||||
|
```cool
|
||||||
|
release_then(db, pool, l, db.query(l, sql))
|
||||||
|
^ ^
|
||||||
|
소비 사용
|
||||||
|
```
|
||||||
|
```
|
||||||
|
l은(는) 이미 move되었습니다 (13:41에서 소비)
|
||||||
|
```
|
||||||
|
|
||||||
|
move 검사가 인자를 왼쪽부터 걷기 때문이다. **실제 평가 순서와 무관하다** —
|
||||||
|
사람은 "질의가 먼저 돌고 그다음 반납"이라고 읽지만 검사기는 그렇게 안 본다.
|
||||||
|
|
||||||
|
우회는 쉽다. 먼저 `let`으로 묶고 넘긴다. 다만 **왜 안 되는지가 코드에서
|
||||||
|
안 보이므로** 진단 메시지가 위치만 말하고 이유를 말하지 않는다.
|
||||||
|
|
||||||
|
### 두 사례 비교 — 이것이 `with` 결정의 근거다
|
||||||
|
|
||||||
|
| | naive | careful | 배수 | match 중첩 | 정리 도우미 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Atomic Updater | 18 | 58 | **3.2** | **4단** | 2개 |
|
||||||
|
| DB Pool | 25 | 58 | **2.3** | **1단** | 2개 |
|
||||||
|
|
||||||
|
**공통 (2/2)**
|
||||||
|
|
||||||
|
1. **자원을 들면 `?`를 한 번도 못 쓴다.** 예외 없이 그렇다
|
||||||
|
2. **"정리하고 결과를 그대로 실어나르는 도우미"를 양쪽이 각자 발명했다**
|
||||||
|
(`abandon`/`discard`, `release_then`/`close_then`). 같은 패턴이 두 번
|
||||||
|
독립적으로 나온 것은 그것이 진짜 추상이라는 뜻이다
|
||||||
|
3. **누수는 조용히 통과한다** — 검사기가 아무 말도 안 한다
|
||||||
|
|
||||||
|
**다른 점 — 그리고 이게 중요하다**
|
||||||
|
|
||||||
|
파일은 4단 중첩이고 풀은 1단이다. 이유는 **정리가 단계마다 다른가**이다.
|
||||||
|
|
||||||
|
- 파일: 닫기 전이면 `close + remove`, 닫은 뒤면 `remove`만 → 단계마다 다름
|
||||||
|
- 풀: 성공이든 실패든 언제나 `release` → 하나로 접힘
|
||||||
|
|
||||||
|
즉 **고통이 자원 개수가 아니라 정리의 균일성에 비례한다.** `with`의 값도
|
||||||
|
그만큼 달라진다.
|
||||||
|
|
||||||
|
### D9. `with`가 성립하려면 D4를 먼저 풀어야 한다
|
||||||
|
|
||||||
|
풀 사례가 `with`의 설계 제약 하나를 드러냈다.
|
||||||
|
|
||||||
|
```cool
|
||||||
|
with l = db.acquire(pool)? { ... } // 나갈 때 무엇을 부르나?
|
||||||
|
```
|
||||||
|
|
||||||
|
반납은 `db.release(pool, l)`이다 — **풀과 권한이 필요하다.** 그런데 `with`가
|
||||||
|
자동으로 부르려면 **자원만으로 정리를 표현할 수 있어야 한다.** 즉:
|
||||||
|
|
||||||
|
```cool
|
||||||
|
resource Lease {
|
||||||
|
release close(own self) effects {Db.release} -> Result[Unit, DbError]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`own self`가 필요하다. 그런데 **capability 메서드는 수신자를 소비할 수
|
||||||
|
없다(D4)**. `own`은 파라미터에만 붙는다.
|
||||||
|
|
||||||
|
파일 쪽도 같다 — `Fs.close(own f)`는 `Fs` 권한을 요구하므로 자원만으로는
|
||||||
|
정리가 안 된다.
|
||||||
|
|
||||||
|
**따라서 순서가 정해진다: D4 → resource/with.** 자원이 자기 정리를 스스로
|
||||||
|
선언할 수 있어야 `with`가 성립한다. 런타임이 lease 안에 풀을 넣어 두면
|
||||||
|
되므로 구현상의 문제는 아니지만, **언어에 표현할 자리가 없다.**
|
||||||
|
|
||||||
|
→ D4를 고쳤다. `Lease.release(own self)`에는 풀도 `Db`도 없다. 전제가
|
||||||
|
충족됐다.
|
||||||
|
|
||||||
|
### 잘 된 것 — 설계가 버틴 부분
|
||||||
|
|
||||||
|
- **두 층 자원이 자연스럽게 중첩된다.** 풀이 바깥, lease가 안쪽
|
||||||
|
- **반납 ≠ 파괴가 시그니처로 표현된다.** `release(p: Pool, own l: Lease)`
|
||||||
|
- **획득 실패가 `Result`로 자연스럽다.** `Exhausted`가 호출자에게 선택을 준다
|
||||||
|
- `List[String]`의 `List`는 내장 타입이라 import가 필요 없다. 모듈 `List`를
|
||||||
|
가져오는 것은 그 타입의 함수를 쓸 때뿐이다 — 미사용 import lint가 정확히
|
||||||
|
이것을 지적했다
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LRU 캐시 (2026-08-30)
|
||||||
|
|
||||||
|
목적: 앞의 둘이 못 건드린 데를 친다. 파일과 풀은 정리 지점이 **렉시컬**이었다.
|
||||||
|
LRU는 아니다 — **축출은 삽입의 부작용으로 예측할 수 없는 때에 일어난다.**
|
||||||
|
|
||||||
|
산출물: `lru-cache/values.cool` 82줄, `lru-cache/resources.cool` 73줄.
|
||||||
|
|
||||||
|
### D10. Option/Result 패턴이 타입을 통째로 잃고 있었다 — 고침. 큰 것이었다
|
||||||
|
|
||||||
|
```cool
|
||||||
|
pub fn probe3(r: Result[Int, String]) -> String {
|
||||||
|
match r {
|
||||||
|
Ok(v) => v, // Int를 String 자리에 둔다
|
||||||
|
Err(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**통과했다.** 원인: typecheck의 `env.ctors`에 내장 생성자가 없었다. 그래서
|
||||||
|
`None`은 생성자가 아니라 **`None`이라는 이름의 변수 바인딩**이 되고,
|
||||||
|
`Ok(v)`의 `v`는 `TUnknown`이 되어 무엇과도 맞았다.
|
||||||
|
|
||||||
|
즉 **이 언어의 핵심 오류 처리 수단인 Option/Result의 패턴 매칭이 타입을 전혀
|
||||||
|
검사하지 않고 있었다.** 소진성 검사는 자기 `is_ctor`에 내장을 갖고 있어서
|
||||||
|
이 사실을 덮고 있었다 — 빠진 경우는 잡으면서 타입은 안 봤다.
|
||||||
|
|
||||||
|
내장 열거형을 등록해서 고쳤다. 기존 코드는 하나도 안 깨졌다.
|
||||||
|
|
||||||
|
이 버그가 여태 안 보인 이유가 씁쓸하다. `match r { Ok(v) => Int.show(v) }`
|
||||||
|
같은 코드는 v가 TUnknown이어도 통과하므로 **아무도 이상함을 못 느낀다.**
|
||||||
|
틀린 코드를 써봐야 드러난다.
|
||||||
|
|
||||||
|
### D11. struct 필드와 affine 값 — 그리고 여기서 보안 구멍이 나왔다
|
||||||
|
|
||||||
|
처음 이렇게 적었다: "필드 접근은 빌림이라 affine 값을 꺼낼 수 없다."
|
||||||
|
**규칙으로는 맞았는데 구현이 그것을 강제하지 않고 있었다.**
|
||||||
|
|
||||||
|
나중에 "다 고쳤나"를 확인하려고 전부 다시 돌려보다 드러났다:
|
||||||
|
|
||||||
|
```cool
|
||||||
|
pub capability Pay { fn charge(n: Int) effects {Pay.charge} }
|
||||||
|
pub struct Wrapper { pay: Pay }
|
||||||
|
pub fn consume(own p: Pay) effects {Pay.charge}
|
||||||
|
|
||||||
|
pub fn duplicate(w: Wrapper) effects {Pay.charge} {
|
||||||
|
consume(w.pay)
|
||||||
|
consume(w.pay) // 같은 capability를 두 번 소비한다
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**통과했다.** 그것도 `w`가 **빌린 값**인데도. 즉 **보안 정리 (ii)("safe
|
||||||
|
code에서 capability는 복제·위조되지 않는다")가 깨져 있었다.** 이 세션에서
|
||||||
|
찾은 것 중 가장 심각하다.
|
||||||
|
|
||||||
|
원인: `E_field`가 빌린 값(`v_use = true`)을 돌려주는데, **`Move` 문맥에서
|
||||||
|
그것을 검사하는 곳이 `E_ident` 분기에만 있었다.** 필드 접근은 그 분기를
|
||||||
|
지나가지 않는다.
|
||||||
|
|
||||||
|
고쳤다. `E_field`가 Move 자리에 놓이고 필드가 affine이면 오류다:
|
||||||
|
|
||||||
|
```
|
||||||
|
pay 필드는 affine이라 다른 함수에 넘길 수 없습니다
|
||||||
|
(v0에는 부분 move가 없습니다 — 꺼내려면 열거형으로 감싸십시오)
|
||||||
|
```
|
||||||
|
|
||||||
|
필드의 affinity를 알려면 바인딩의 선언 타입이 필요해서, move 검사기에
|
||||||
|
struct 필드 표와 바인딩 타입을 넣었다. **copyable 필드는 막지 않는다** —
|
||||||
|
`w.label`은 통과한다.
|
||||||
|
|
||||||
|
그리고 이것이 LRU에서 열거형으로 우회한 것을 사후에 정당화한다. 그때는
|
||||||
|
"struct로는 안 되고 열거형으로는 된다"가 우연처럼 보였는데, **열거형이
|
||||||
|
유일한 길인 것이 규칙이었고 struct 쪽이 새고 있었을 뿐이다.**
|
||||||
|
|
||||||
|
#### 이 버그가 여태 안 보인 이유
|
||||||
|
|
||||||
|
`samples/05_move_errors.cool`은 자원 타입을 **직접** 다룬다. struct에 넣고
|
||||||
|
필드로 꺼내는 코드가 없었다. 개밥 먹기에서 **자원을 자료구조에 담는** 코드를
|
||||||
|
처음 쓰면서 드러났다.
|
||||||
|
|
||||||
|
#### (원래 기록) 열거형으로만 둘을 함께 돌려줄 수 있다
|
||||||
|
|
||||||
|
`put`은 캐시와 축출된 자원을 **함께** 돌려줘야 한다. 튜플이 없으니 struct다.
|
||||||
|
|
||||||
|
```cool
|
||||||
|
pub struct Put { cache: LeaseCache, evicted: Option[Db.Lease] }
|
||||||
|
...
|
||||||
|
match p.evicted { Some(l) => release(l), ... }
|
||||||
|
```
|
||||||
|
```
|
||||||
|
l은(는) 빌린 값이라 다른 함수에 넘길 수 없습니다
|
||||||
|
```
|
||||||
|
|
||||||
|
**필드 접근은 빌림이고, struct를 분해하는 패턴이 언어에 없다.** 그래서 struct에
|
||||||
|
넣은 affine 값은 다시 꺼낼 수 없다.
|
||||||
|
|
||||||
|
열거형은 된다 — 패턴이 분해하기 때문이다:
|
||||||
|
|
||||||
|
```cool
|
||||||
|
pub enum Put {
|
||||||
|
Kept(LeaseCache),
|
||||||
|
Evicted(LeaseCache, Db.Lease),
|
||||||
|
}
|
||||||
|
match p { Evicted(c, l) => release_then(db, pool, l, c), ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
**즉 "둘을 함께 돌려주기"가 열거형으로만 가능하다.** 튜플이 없는 대가가
|
||||||
|
여기서 두 번째로 나온다(F5에 이어). 그리고 이건 취향 문제가 아니라
|
||||||
|
**표현 가능성 문제**다.
|
||||||
|
|
||||||
|
→ 해법 후보: struct 분해 패턴 + 부분 이동, 또는 튜플. 둘 다 문법 결정이다.
|
||||||
|
|
||||||
|
### D12. 축출은 렉시컬이 아니다 — `with`가 못 닿는 첫 자리
|
||||||
|
|
||||||
|
`with`는 렉시컬 수명만 다룬다. 축출된 lease는 **어느 블록에도 묶이지 않는다.**
|
||||||
|
삽입할 때 튀어나오고, 그 시점은 캐시 상태에 달려 있다.
|
||||||
|
|
||||||
|
그래서 `resource`가 렉시컬 전용이면 **자원을 담는 컨테이너를 언어로 만들 수
|
||||||
|
없다.** 그리고 이것이 이미 우리 설계에 나타나 있었다 — `Pool` 자체가 자원
|
||||||
|
캐시인데, `std-draft/db.cool`에서 **런타임이 주는 불투명한 capability**로
|
||||||
|
선언했다. 만들 수 없어서 그렇게 한 것이다.
|
||||||
|
|
||||||
|
두 갈래다:
|
||||||
|
|
||||||
|
- **(a) 자원은 렉시컬 전용** → 자원 컨테이너는 런타임만 만들 수 있다.
|
||||||
|
풀, 캐시, 레지스트리가 전부 언어 밖이 된다. 단순하고, 지금 상태가 그렇다
|
||||||
|
- **(b) 자원이 자료구조로 탈출할 수 있다** → `with`만으로 부족하고 결국
|
||||||
|
선형 타입이 필요하다. v0가 미룬 바로 그것
|
||||||
|
|
||||||
|
**(a)를 권한다.** 자원 컨테이너는 드물고, 만드는 쪽은 런타임이며, 쓰는 쪽은
|
||||||
|
lease를 렉시컬하게 빌린다. 다만 **이 선택이 무엇을 포기하는지 적어둬야
|
||||||
|
한다** — 사용자가 자기 자원 풀을 언어로 못 만든다.
|
||||||
|
|
||||||
|
### 자료구조 자체의 마찰 (값 판)
|
||||||
|
|
||||||
|
- **Map이 없어 O(n)이다.** LRU의 요점이 O(1)인데 표현할 수단이 없다
|
||||||
|
- **튜플이 없어 운반용 struct를 세 번 만들었다** — `Got`, `Put`, `Dropping`
|
||||||
|
- **`get`이 캐시를 새로 돌려줘야 한다.** 최근성이 바뀌므로. 공유 가변 상태가
|
||||||
|
없다는 것의 대가이고, 호출자가 캐시를 계속 실어날라야 한다
|
||||||
|
- **뒤에서 자르는 함수가 std에 없다.** `drop_last`를 `reverse` 두 번으로 썼다
|
||||||
|
- **fold에 "첫 원소 건너뛰기"가 없어** `Dropping` struct를 또 만들었다
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 결정 — resource / with
|
||||||
|
|
||||||
|
**넣는다. 다만 D4가 먼저다.**
|
||||||
|
|
||||||
|
근거는 위 표다. 데이터 두 점에서 공통으로:
|
||||||
|
- `?`를 자원과 함께 쓸 수 없다 (2/2)
|
||||||
|
- 같은 정리 패턴을 각자 발명했다 (2/2)
|
||||||
|
- 누수가 조용히 통과한다 (2/2)
|
||||||
|
- 2.3 ~ 3.2배
|
||||||
|
|
||||||
|
그리고 `with`가 암묵적 drop보다 나은 이유가 여기서도 확인된다 — 정리 지점이
|
||||||
|
닫는 중괄호로 보이고, 정리의 effect가 effects 절에 나타나 검사된다.
|
||||||
|
|
||||||
|
## Mini Shell (2026-08-30)
|
||||||
|
|
||||||
|
목적: 앞의 셋이 못 건드린 모양을 친다.
|
||||||
|
|
||||||
|
| | 자원 개수 | 정리 지점 | 한 자원이 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 파일 | 하나 | 렉시컬 | 한 곳으로 |
|
||||||
|
| 풀 | 두 층 | 렉시컬 | 한 곳으로 |
|
||||||
|
| LRU | 한 종류, 여럿 | **비렉시컬** | 한 곳으로 |
|
||||||
|
| **셸** | **여러 종류, 여럿** | 비렉시컬 | **둘로 갈라져 서로 다른 곳으로** |
|
||||||
|
|
||||||
|
파이프 하나는 끝이 둘이고 각각 다른 자식에게 간다. 부모는 넘긴 뒤 아무것도
|
||||||
|
안 남는다. **어느 블록에도 묶이지 않는다.**
|
||||||
|
|
||||||
|
산출물: `std-draft/os.cool` 95줄, `mini-shell/parse.cool` 130줄,
|
||||||
|
`mini-shell/shell.cool` 155줄.
|
||||||
|
|
||||||
|
**처음으로 개밥이 실제로 돈다.** 파싱을 자원에서 떼어 놓았기 때문이다 —
|
||||||
|
`parse.cool`의 내장 test 5개가 `coolc test`로 통과한다. capability를 받지
|
||||||
|
않으므로 effect-free임이 검사로 증명되고, 그래서 순서에 의존하지 않는다.
|
||||||
|
|
||||||
|
### D13. 열거형 생성자가 인자를 소비하지 않았다 — 고침 (보안)
|
||||||
|
|
||||||
|
```cool
|
||||||
|
fn dup(own f: Fd) -> Option[Fd] {
|
||||||
|
let a = Some(f)
|
||||||
|
Some(f) // 통과했다
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
리스트 리터럴(`[f, f]`)과 struct 리터럴은 처음부터 Move 자리였는데
|
||||||
|
**생성자만 일반 호출로 취급돼 빠져 있었다.** 사용자 열거형도 내장
|
||||||
|
`Some`/`Ok`도 마찬가지였다.
|
||||||
|
|
||||||
|
**LRU가 affine 값을 열거형으로 실어나르는 우회를 쓰므로, 이 자리가
|
||||||
|
뚫려 있으면 그 우회 자체가 무의미했다.** 넣는 쪽이 검사되지 않으면
|
||||||
|
꺼내는 쪽을 아무리 조여도 소용이 없다.
|
||||||
|
|
||||||
|
### D14. 소유한 열거형을 두 번 분해할 수 있었다 — 고침 (보안)
|
||||||
|
|
||||||
|
```cool
|
||||||
|
fn dup(own p: Pipe) effects {Fd.close} {
|
||||||
|
match p { Ends(a, b) => { take(a) take(b) }, }
|
||||||
|
match p { Ends(a, b) => { take(a) take(b) }, } // 통과했다
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`match`가 scrutinee를 항상 빌렸다. 조각은 owned로 꺼내 놓고 원본은
|
||||||
|
소비하지 않았으므로 **같은 파이프에서 같은 fd를 두 번 꺼낼 수 있었다.**
|
||||||
|
|
||||||
|
이름을 묶는 패턴으로 소유한 affine 값을 분해하면 그 값이 소비되게 했다.
|
||||||
|
이름을 묶지 않는 패턴(`Ends(_, _)`)은 아무것도 꺼내지 않으므로 그대로 둔다.
|
||||||
|
|
||||||
|
**D11·D13과 같은 하나의 규칙이 세 자리에서 새고 있었다**: v0에 부분 move가
|
||||||
|
없으므로 affine 값이 합성 구조를 드나드는 모든 자리는 Move 자리여야 한다.
|
||||||
|
struct 필드(D11), 열거형 생성자(D13), 열거형 분해(D14) — 셋 다 뚫려 있었다.
|
||||||
|
남은 자리를 세어 보면 리스트 리터럴, struct 리터럴, 함수 인자, 반환,
|
||||||
|
클로저 capture이고 이들은 처음부터 막혀 있었다.
|
||||||
|
|
||||||
|
### D15. 점 왼쪽을 검사하지 않았다 — 고침 (D1과 같은 병)
|
||||||
|
|
||||||
|
```cool
|
||||||
|
fn f() -> Bool {
|
||||||
|
Nonsense.whatever(1) // 통과했다
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Nonsense`는 어디에도 없다. 그런데 "모르는 것을 틀렸다고 말하지 않는다"는
|
||||||
|
규칙이 **가져오지도 않은 이름에까지 적용되고 있었다.** 그 규칙은 해소할 수
|
||||||
|
없는 패키지 import를 위한 것이지, 아무 이름이나 써도 된다는 뜻이 아니다.
|
||||||
|
|
||||||
|
내장 타입 이름도 모듈이 아니다. 타입으로 쓰는 데는 import가 필요 없지만
|
||||||
|
그 타입에 딸린 함수를 쓰려면 가져와야 한다 — prelude가 없다는 규칙에
|
||||||
|
예외를 두지 않는다.
|
||||||
|
|
||||||
|
**samples 셋이 이 구멍에 기대고 있었다** (02, 04, 06). `Code.show`,
|
||||||
|
`File.close`가 어디에도 정의되지 않은 채 통과하고 있었다. 셋 다 고쳤고,
|
||||||
|
06은 `own self` 덕에 `File.close(f)`가 `f.close()`가 됐다.
|
||||||
|
|
||||||
|
### D16. variant 이름이 모듈 전역이다 — 열림
|
||||||
|
|
||||||
|
```cool
|
||||||
|
pub enum Stdin { Inherit, FromPipe(ReadFd) }
|
||||||
|
pub enum Stdout { Inherit, ToPipe(WriteFd) } // variant Inherit이 중복입니다
|
||||||
|
```
|
||||||
|
|
||||||
|
`InheritIn` / `InheritOut`이 됐다. **뜻이 아니라 언어의 제약이 고른
|
||||||
|
이름이다** — 철학 6이 지지 않아야 할 방식으로 진 자리다.
|
||||||
|
|
||||||
|
원인은 `match` 팔에 열거형 이름을 적지 않는다는 결정이다 (한 개념 한 방식).
|
||||||
|
그 결정 자체는 옳아 보이는데 대가가 여기서 나왔다. 고치려면 `Stdin.Inherit`
|
||||||
|
같은 한정 표기를 식과 패턴 양쪽에 넣어야 하고, 그러면 "한 방식"이 둘이 된다.
|
||||||
|
|
||||||
|
**미룬다.** 두 열거형이 같은 뜻의 variant를 갖는 일은 흔하지만, 지금은
|
||||||
|
데이터 점이 하나뿐이다. Reverse Proxy나 TCP Server에서 또 나오면 그때
|
||||||
|
결정한다.
|
||||||
|
|
||||||
|
### D17. 리스트에 넣은 자원을 꺼낼 수 없다 — 열림, 설계 결정 대기
|
||||||
|
|
||||||
|
셸의 자연스러운 모양은 "자식을 전부 띄우고 리스트에 모아 두었다가 한꺼번에
|
||||||
|
거두기"다. 파이프 버퍼가 차면 막히므로 **띄우기를 다 끝내기 전에 거둘 수
|
||||||
|
없다.**
|
||||||
|
|
||||||
|
넣는 것은 된다 — `[c1, c2]`는 통과하고 `List[Child]`는 전이적으로 affine이다.
|
||||||
|
**꺼내는 것이 안 된다.** `List.fold`의 시그니처가 `fn(own acc, a)`라 원소 `a`는
|
||||||
|
빌린 값이고, 빌린 자원은 소비할 수 없다.
|
||||||
|
|
||||||
|
그래서 재귀가 됐다 — "뒤를 먼저 다 띄우고, 돌아오면서 자기 자식을 거둔다".
|
||||||
|
읽기에 나쁘지 않고 오히려 파이프라인의 모양과 닮았다. 하지만 이것은 우회지
|
||||||
|
답이 아니다.
|
||||||
|
|
||||||
|
이것이 **D12(축출은 비렉시컬)의 반대편**이다. D12는 자원이 컨테이너에서
|
||||||
|
예측 못 할 때 나가는 문제였고, 여기는 아예 나갈 방법이 없는 문제다. 둘이
|
||||||
|
같은 결정을 기다린다: **자원을 담는 컨테이너를 사용자가 만들 수 있는가.**
|
||||||
|
|
||||||
|
### D18. `spawn`이 자원을 가져갔는지 시그니처로 알 수 없다 — 열림
|
||||||
|
|
||||||
|
```cool
|
||||||
|
fn spawn(program: String, args: List[String],
|
||||||
|
own stdin: Stdin, own stdout: Stdout)
|
||||||
|
effects {Os.spawn} -> Result[Child, OsError]
|
||||||
|
```
|
||||||
|
|
||||||
|
`own`이 있으므로 부르는 쪽에서는 소비된다. 그런데 **실패했을 때 안에서
|
||||||
|
정리됐는지는 시그니처가 말하지 않는다.** 실제 `posix_spawn`은 실패해도 fd를
|
||||||
|
닫지 않는다 — 즉 이 시그니처는 진실을 감춘다.
|
||||||
|
|
||||||
|
정직하게 쓰면 `Result[Child, SpawnFailed]`이고 `SpawnFailed`가 fd를 돌려줘야
|
||||||
|
한다. 그러면 실패 처리가 매번 두 겹이 된다.
|
||||||
|
|
||||||
|
**이것은 언어의 결함이 아니라 계약의 문제다.** 다만 언어가 `own`을 강제하는
|
||||||
|
순간 이 질문이 모든 자원 API에서 나온다는 것을 기록해 둔다 — Rust도
|
||||||
|
`std::process`에서 같은 선택을 했고 fd를 돌려주지 않는다.
|
||||||
|
|
||||||
|
### 잘 된 것
|
||||||
|
|
||||||
|
- **파싱과 자원을 나눈 것이 옳았다.** 파싱이 순수하므로 내장 test로 그 자리에서
|
||||||
|
증명된다. 자원이 섞였으면 개밥 먹기 전체가 "쓰기만 한 것"으로 남았다
|
||||||
|
- **`?`가 파싱에서는 필요 없었다.** fold의 누적자가 `Result`라 실패가 그대로
|
||||||
|
실려간다 — 조기 탈출 없이도 뒤 조각이 앞의 실패를 덮지 않는다
|
||||||
|
- **읽는 끝과 쓰는 끝을 타입으로 나눈 것**이 값싸고 정확했다. 읽는 끝에 쓰는
|
||||||
|
것은 프로그램의 결함이고, 결함은 타입이 막는 편이 낫다
|
||||||
|
- **`own self` 뒤로 `child.wait()`가 자연스럽다.** 1판 문법이었다면
|
||||||
|
`os.wait(child)`였고 `Os` 권한을 자식 거두기까지 들고 다녀야 했다
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 테스트 하네스의 결함 (2026-08-30)
|
||||||
|
|
||||||
|
개밥 먹기가 아니라 저장소 자체에서 나온 것이므로 따로 적는다.
|
||||||
|
|
||||||
|
`test/test_coollang.ml`의 요약과 `exit 1`이 **파일 184줄**에 있었다. OCaml의
|
||||||
|
최상위 `let ()`은 파일 순서대로 돌므로 **그 뒤에 쓴 1500줄의 검사는 FAIL을
|
||||||
|
찍고도 종료 코드가 0이었다.**
|
||||||
|
|
||||||
|
즉 **실패한 테스트를 커밋할 수 있었다.** 빠른 검증 루프가 목적인 저장소에서
|
||||||
|
검증이 조용히 통과하는 것이 가장 나쁜 결함이다. 요약을 파일 끝으로 옮겼다.
|
||||||
|
|
||||||
|
발견 경위가 이 저장소의 방법론을 그대로 보여준다 — 새 테스트를 넣고
|
||||||
|
"일부러 틀리게 만들어 FAIL이 나는지" 확인하다가 나왔다. 통과를 믿지 않고
|
||||||
|
실패를 확인하는 습관이 아니었으면 안 보였다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**순서** (2026-08-30 갱신 — 1·3 완료)
|
||||||
|
|
||||||
|
1. ~~**D4** — 수신자를 소비하는 메서드~~ → 고침. `own self`
|
||||||
|
2. **`resource` 종류와 `with`** — 아래 "with를 미루는 이유" 참고
|
||||||
|
3. ~~Mini Shell~~ → 완료. D13–D18이 여기서 나왔다
|
||||||
|
|
||||||
|
**LRU가 더한 제약**: 자원은 렉시컬 전용으로 간다. 자원을 담는 컨테이너는
|
||||||
|
런타임만 만들 수 있다 — 축출처럼 비렉시컬한 정리는 `with`가 못 닿기 때문이다.
|
||||||
|
사용자가 자기 자원 풀을 언어로 못 만드는 것이 그 대가다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `with`를 미루는 이유 (2026-08-30 결정)
|
||||||
|
|
||||||
|
네 개밥을 다 쓰고 나서 내린 결정이다. 셋을 정하고 하나를 미룬다.
|
||||||
|
|
||||||
|
### 1. `resource`라는 새 종류는 넣지 않는다 — 확정
|
||||||
|
|
||||||
|
capability가 이미 그것이다. affine이고, 위조되지 않고, 런타임만 만든다.
|
||||||
|
`resource`가 더하는 사실은 "정리 메서드가 있다" 하나뿐인데, 그것은 `own self`가
|
||||||
|
말한다. 종류를 하나 더 만들면 **95%가 같은 두 종류**가 생기고 그것이 "한 개념
|
||||||
|
한 방식"이 금지하는 모양이다.
|
||||||
|
|
||||||
|
### 2. 누수는 lint로 보인다 — 넣음
|
||||||
|
|
||||||
|
D3의 답을 절반 냈다. `?`와 `return`이 있는 자리에서, 소유하고 있고 아직 아무
|
||||||
|
데도 넘기지 않은 affine 값이 있으면 말한다.
|
||||||
|
|
||||||
|
```
|
||||||
|
?이(가) 여기서 빠져나가면 f이(가) 정리되지 않습니다 (?는 뒤의 정리를 건너뜁니다)
|
||||||
|
```
|
||||||
|
|
||||||
|
**측정**: 저장소 전체에서 네 곳. `atomic-update/naive` 2, `db-pool/naive` 2.
|
||||||
|
**오탐 0건** — careful 판 둘과 samples 전부가 조용하다. naive/careful 쌍이
|
||||||
|
정확히 나뉜다.
|
||||||
|
|
||||||
|
타입 층의 정책은 그대로다 — 오용 금지, 누수 허용. lint는 막지 않고 말한다.
|
||||||
|
|
||||||
|
### 3. `with`는 미룬다 — 설계가 갈라져 있고, 데이터가 아직 부족하다
|
||||||
|
|
||||||
|
두 갈래가 있고 둘 다 그럴듯하다.
|
||||||
|
|
||||||
|
**갈래 A — 자동 반납.** 블록을 나갈 때 런타임이 정리 메서드를 부른다
|
||||||
|
(Python `with`, C# `using`). `?`가 그대로 쓸 수 있게 되고 `careful.cool`
|
||||||
|
58줄이 `naive.cool` 18줄로 접힌다. 대가: **선언되지 않은 effect가 수행된다.**
|
||||||
|
이 언어에서 effect는 시그니처에 다 적혀야 하는데, 안 보이는 호출이 생긴다.
|
||||||
|
그리고 정리 메서드가 둘인 자원(트랜잭션의 commit/abort)을 다루지 못한다.
|
||||||
|
|
||||||
|
**갈래 B — 소비 강제.** 블록이 끝나기 전에 모든 길에서 그 값을 소비해야
|
||||||
|
하고, 아니면 오류다. 아무것도 자동으로 부르지 않는다. 숨은 effect가 없고
|
||||||
|
commit/abort가 자연스럽다. 대가: **마찰이 그대로 남는다.** 컴파일러가
|
||||||
|
"여기서 안 닫았다"고 말해 줄 뿐, 중첩된 match를 손으로 쓰는 일은 똑같다.
|
||||||
|
|
||||||
|
**미루는 이유는 Mini Shell이다.** 셸이 보여준 모양에는 `with`가 닿지 않는다.
|
||||||
|
파이프 하나는 끝이 둘이고 각각 다른 자식에게 간다. 부모에게는 아무것도 안
|
||||||
|
남고, 어느 블록에도 묶이지 않는다. 즉 **가장 어려운 자원 코드가 렉시컬하지
|
||||||
|
않다.** 렉시컬 도구를 먼저 넣으면 그 도구가 닿지 않는 자리를 나중에 다시
|
||||||
|
설계해야 하고, 그때는 이미 두 방식이 공존한다.
|
||||||
|
|
||||||
|
`own self`가 D9이 요구한 것을 이미 줬으므로 급하지 않다. 정리는 자원만으로
|
||||||
|
표현되고, 도우미의 시그니처가 진짜 의존을 말한다.
|
||||||
|
|
||||||
|
**결정을 다시 여는 조건**: Process Supervisor(#3)나 Reverse Proxy(#4)에서
|
||||||
|
렉시컬한 자원 코드가 다수를 차지하면 갈래 A·B 중 하나를 고른다. 셸처럼
|
||||||
|
비렉시컬한 것이 또 나오면 `with`가 아니라 컨테이너 문제(D17)를 먼저 푼다.
|
||||||
|
|
||||||
|
**미루는 것**: crash 시 정리 여부. `with`를 넣어도 이 질문은 열려 있다 —
|
||||||
|
정상 종료·`return`·`?`에서는 돌고 crash에서는 안 도는 것이 지금의 잠정
|
||||||
|
답이지만, 감독(supervision)이 들어오면 다시 봐야 한다.
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# dogfoods — 실제 유스케이스로 언어를 압박한다
|
||||||
|
|
||||||
|
`samples/`가 검사기를 시험한다면 여기는 **언어가 실제 문제를 표현할 수 있는지**를
|
||||||
|
시험한다. 성격이 다르므로 폴더를 나눈다.
|
||||||
|
|
||||||
|
## 방법
|
||||||
|
|
||||||
|
프로그램을 **쓰되 돌리지 않는다.** `std-draft/`에 런타임이 구현한다고 가정한
|
||||||
|
선언을 두고, 그것에 대고 실제 프로그램을 쓴다.
|
||||||
|
|
||||||
|
"가상"이 아니다. `std/*.cool`이 원래 본문 없는 선언이므로 **실행만 빼고 전부
|
||||||
|
진짜로 검사된다**:
|
||||||
|
|
||||||
|
```
|
||||||
|
lexer ✓ parser ✓ 이름 해소 ✓ 타입 ✓ effect ✓
|
||||||
|
capability ✓ move/affinity ✓ 소진성 ✓ interface hash ✓
|
||||||
|
run ✗
|
||||||
|
```
|
||||||
|
|
||||||
|
종이 스케치가 아니라 컴파일러가 검증한 설계다.
|
||||||
|
|
||||||
|
## 합격 기준
|
||||||
|
|
||||||
|
```
|
||||||
|
① coolc check exit 0
|
||||||
|
② 모듈이 실제로 해소될 것
|
||||||
|
```
|
||||||
|
|
||||||
|
②가 없으면 ①이 공허하다. import가 조용히 해소되지 않으면 그 모듈의 이름이
|
||||||
|
전부 불투명해지고, 검사기는 "모르는 것을 틀렸다고 말하지 않는다"는 원칙에 따라
|
||||||
|
무엇이든 통과시킨다. **실제로 첫 시도에서 이 일이 일어났다** (D1 참고).
|
||||||
|
|
||||||
|
## 규율
|
||||||
|
|
||||||
|
`std-draft`의 선언은 독립적으로 정당화되어야 한다.
|
||||||
|
|
||||||
|
- 지금 런타임이 가진 권한으로 구현 가능할 것
|
||||||
|
- effect를 전부 선언할 것
|
||||||
|
- **실패 양상을 선언하는 자리에서 정할 것** — 무엇이 `Result`이고 무엇이
|
||||||
|
`crash`인지
|
||||||
|
- 없는 언어 기능에 기대지 말 것. 필요하다는 것이 드러나면 **그것이 발견이지
|
||||||
|
지름길이 아니다**
|
||||||
|
|
||||||
|
프로그램이 예뻐 보이도록 API를 발명하면 아무것도 배우지 못한다.
|
||||||
|
|
||||||
|
## 한계
|
||||||
|
|
||||||
|
이 기준이 증명하는 것은 **표현 가능성**이지 의미론의 정확성이 아니다.
|
||||||
|
`check`가 통과한다고 그 프로그램이 옳게 도는 것은 아니다 — 돌려본 적이 없다.
|
||||||
|
|
||||||
|
**Mini Shell에서 그 한계가 반쯤 깨졌다.** 파싱을 자원에서 떼어 놓았더니
|
||||||
|
`mini-shell/parse.cool`은 실제로 돈다 — 내장 test 5개가 `coolc test`로
|
||||||
|
통과한다. 자원을 안 쓰는 부분은 돌릴 수 있다는 뜻이고, 그것은 개밥을
|
||||||
|
어떻게 나눠 써야 하는지를 말해 준다.
|
||||||
|
|
||||||
|
## 세트
|
||||||
|
|
||||||
|
| | | 압박하는 곳 | 상태 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | **Mini Shell** | **여러 자원, 하나가 둘로 갈라짐** | **완료** |
|
||||||
|
| 2 | **Atomic File Updater** | **자원 수명, 실패 경로** | **완료** |
|
||||||
|
| 3 | Process Supervisor | 실패 위상 | 대기 |
|
||||||
|
| 4 | Reverse Proxy | 취소, backpressure | 대기 |
|
||||||
|
| 5 | TCP Server | 규모의 API 모양 | 대기 |
|
||||||
|
| 6 | DB Pool | lease, 자원 정리 | **완료** |
|
||||||
|
| + | **LRU Cache** | **비렉시컬 정리 (축출)** | **완료** |
|
||||||
|
| 7 | Resource Exhaustion | 실패 의미론의 최악 조건 | **미룸** — 런타임 속성이라 안 돌리면 알 수 없다. 언어 층 질문("무엇이 Result이고 무엇이 crash인가")은 std-draft 규율로 흡수했다 |
|
||||||
|
|
||||||
|
순서는 자원 모델(2, 6, 1) → 동시성·취소(3, 4, 5)다. 2번을 먼저 둔 이유는
|
||||||
|
**동시성을 안 섞고 자원 모델만 시험할 수 있는 유일한 것**이기 때문이다.
|
||||||
|
|
||||||
|
자원 모델 쪽 넷이 끝났다. 넷이 각각 다른 모양이었다:
|
||||||
|
|
||||||
|
| | 자원 개수 | 정리 지점 | 한 자원이 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 파일 | 하나 | 렉시컬 | 한 곳으로 |
|
||||||
|
| 풀 | 두 층 | 렉시컬 | 한 곳으로 |
|
||||||
|
| LRU | 한 종류, 여럿 | 비렉시컬 | 한 곳으로 |
|
||||||
|
| 셸 | 여러 종류, 여럿 | 비렉시컬 | **둘로 갈라져 서로 다른 곳으로** |
|
||||||
|
|
||||||
|
이 넷이 `with`를 미루기로 한 근거다 (FINDINGS의 "`with`를 미루는 이유").
|
||||||
|
|
||||||
|
발견은 `FINDINGS.md`에 쌓고, 확정된 결정은 `docs/thesis.md`로 올린다.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// 제대로 정리하는 버전.
|
||||||
|
//
|
||||||
|
// naive.cool과 같은 일을 하지만 모든 실패 경로에서 핸들을 닫고 임시 파일을
|
||||||
|
// 지운다. 두 파일의 차이가 이 개밥 먹기의 산출물이다.
|
||||||
|
//
|
||||||
|
// ※ 2판. `own self` 뒤에 다시 썼다. 도우미들이 더 이상 fs를 들고 다니지
|
||||||
|
// 않는다 — 닫는 데 필요한 것이 핸들 자신뿐이기 때문이다.
|
||||||
|
|
||||||
|
import "../std-draft/fs" as Fs
|
||||||
|
import "cool.dev/std/string" as String
|
||||||
|
|
||||||
|
pub fn update(fs: Fs.Fs, path: String, contents: String)
|
||||||
|
effects {Fs.create, WriteFile.write, WriteFile.sync, WriteFile.close,
|
||||||
|
Fs.rename, Fs.remove}
|
||||||
|
-> Result[Unit, Fs.IoError] {
|
||||||
|
let tmp = String.concat(path, ".tmp")
|
||||||
|
match fs.create(tmp) {
|
||||||
|
Err(e) => Err(e),
|
||||||
|
Ok(f) => finish(fs, f, tmp, path, contents),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 핸들을 얻은 뒤. 여기서부터 모든 실패 경로가 f를 닫고 tmp를 지워야 한다.
|
||||||
|
// ? 를 쓸 수 없다 — 조기 반환이 정리를 건너뛰기 때문이다.
|
||||||
|
pub fn finish(
|
||||||
|
fs: Fs.Fs,
|
||||||
|
own f: Fs.WriteFile,
|
||||||
|
tmp: String,
|
||||||
|
path: String,
|
||||||
|
contents: String,
|
||||||
|
) effects {WriteFile.write, WriteFile.sync, WriteFile.close, Fs.rename, Fs.remove}
|
||||||
|
-> Result[Unit, Fs.IoError] {
|
||||||
|
match f.write(contents) {
|
||||||
|
Err(e) => abandon(fs, f, tmp, e),
|
||||||
|
Ok(_) => match f.sync() {
|
||||||
|
Err(e) => abandon(fs, f, tmp, e),
|
||||||
|
Ok(_) => match f.close() {
|
||||||
|
Err(e) => discard(fs, tmp, e),
|
||||||
|
Ok(_) => match fs.rename(tmp, path) {
|
||||||
|
Err(e) => discard(fs, tmp, e),
|
||||||
|
Ok(_) => Ok(unit),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 핸들을 아직 들고 있는 실패. 닫고 지운다.
|
||||||
|
// 정리 중의 실패는 일부러 버린다 — 원래 오류가 더 중요하다.
|
||||||
|
// let _ = 가 버린다는 사실을 코드에 보이게 한다.
|
||||||
|
pub fn abandon(fs: Fs.Fs, own f: Fs.WriteFile, tmp: String, e: Fs.IoError)
|
||||||
|
effects {WriteFile.close, Fs.remove} -> Result[Unit, Fs.IoError] {
|
||||||
|
let _ = f.close()
|
||||||
|
discard(fs, tmp, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 핸들은 이미 없다. 임시 파일만 지운다.
|
||||||
|
pub fn discard(fs: Fs.Fs, tmp: String, e: Fs.IoError)
|
||||||
|
effects {Fs.remove} -> Result[Unit, Fs.IoError] {
|
||||||
|
let _ = fs.remove(tmp)
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// 순진한 버전 — ?로 짧게 쓴 것.
|
||||||
|
//
|
||||||
|
// 처음 썼을 때 이 파일의 목적은 "통과한다는 사실을 보여주는 것"이었다.
|
||||||
|
// 이제 lint가 두 자리를 가리킨다 — write와 sync의 ?다. 그 둘에서 빠져나가면
|
||||||
|
// f가 닫히지 않는다. 타입 층에서는 여전히 오류가 아니다 (누수 허용).
|
||||||
|
|
||||||
|
import "../std-draft/fs" as Fs
|
||||||
|
import "cool.dev/std/string" as String
|
||||||
|
|
||||||
|
pub fn update(fs: Fs.Fs, path: String, contents: String)
|
||||||
|
effects {Fs.create, WriteFile.write, WriteFile.sync, WriteFile.close, Fs.rename}
|
||||||
|
-> Result[Unit, Fs.IoError] {
|
||||||
|
let tmp = String.concat(path, ".tmp")
|
||||||
|
let f = fs.create(tmp)?
|
||||||
|
f.write(contents)?
|
||||||
|
f.sync()?
|
||||||
|
f.close()?
|
||||||
|
fs.rename(tmp, path)?
|
||||||
|
Ok(unit)
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// 제대로 반납하는 버전.
|
||||||
|
//
|
||||||
|
// Atomic Updater와 비교하는 것이 목적이다. 자원이 두 층이고, 반납이
|
||||||
|
// 파괴가 아니며, 획득이 실패할 수 있다 — 파일에는 셋 다 없었다.
|
||||||
|
//
|
||||||
|
// ※ 2판. `own self` 뒤에 다시 썼다. 1판과의 차이는 아래 주석에 적는다.
|
||||||
|
|
||||||
|
import "../std-draft/db" as Db
|
||||||
|
|
||||||
|
// 한 요청. 성공하든 실패하든 lease를 반납한다.
|
||||||
|
pub fn load_user(pool: Db.Pool, sql: String)
|
||||||
|
effects {Pool.acquire, Lease.query, Lease.release}
|
||||||
|
-> Result[List[String], Db.DbError] {
|
||||||
|
match pool.acquire() {
|
||||||
|
Err(e) => Err(e),
|
||||||
|
// 1판에서는 l을 소비하는 인자와 l을 쓰는 인자를 한 호출에 섞을 수
|
||||||
|
// 없어(D8) 순서를 손으로 풀어야 했다. 이제 질의는 수신자를 빌리고
|
||||||
|
// 반납은 수신자를 소비하므로 둘이 같은 인자 목록에 만날 일이 없다.
|
||||||
|
Ok(l) => {
|
||||||
|
let r = l.query(sql)
|
||||||
|
release_then(l, r)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 반납하고 원래 결과를 그대로 돌려준다.
|
||||||
|
// 반납 실패는 일부러 버린다 — 원래 결과가 더 중요하다.
|
||||||
|
//
|
||||||
|
// 1판은 (db, pool, l, r) 넷을 받았다. 이제 둘이다 — 반납에 필요한 것이
|
||||||
|
// lease 자신뿐이기 때문이다. 도우미의 시그니처가 정리의 진짜 의존을 말한다.
|
||||||
|
pub fn release_then(
|
||||||
|
own l: Db.Lease,
|
||||||
|
r: Result[List[String], Db.DbError],
|
||||||
|
) effects {Lease.release} -> Result[List[String], Db.DbError] {
|
||||||
|
let _ = l.release()
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
// 프로그램 하나. 풀을 열고 쓰고 닫는다.
|
||||||
|
pub fn run(db: Db.Db, url: String, sql: String)
|
||||||
|
effects {Db.pool, Pool.acquire, Pool.close, Lease.query, Lease.release}
|
||||||
|
-> Result[List[String], Db.DbError] {
|
||||||
|
match db.pool(url, 8) {
|
||||||
|
Err(e) => Err(e),
|
||||||
|
Ok(pool) => {
|
||||||
|
let rows = load_user(pool, sql)
|
||||||
|
close_then(pool, rows)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_then(
|
||||||
|
own pool: Db.Pool,
|
||||||
|
r: Result[List[String], Db.DbError],
|
||||||
|
) effects {Pool.close} -> Result[List[String], Db.DbError] {
|
||||||
|
let _ = pool.close()
|
||||||
|
r
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// 순진한 버전 — ?로 짧게 쓴 것.
|
||||||
|
//
|
||||||
|
// Atomic Updater와 같은 병이 나오는지 본다. 다만 자원이 두 층이다 —
|
||||||
|
// 풀과 lease. 그리고 반납은 파괴가 아니다.
|
||||||
|
//
|
||||||
|
// lint가 두 층을 각각 가리킨다: query의 ?에서 lease가, load_user의 ?에서
|
||||||
|
// 풀이 샌다. 층이 늘면 새는 자리도 그만큼 는다.
|
||||||
|
|
||||||
|
import "../std-draft/db" as Db
|
||||||
|
|
||||||
|
// 한 요청. lease를 빌리고, 질의하고, 반납한다.
|
||||||
|
pub fn load_user(pool: Db.Pool, sql: String)
|
||||||
|
effects {Pool.acquire, Lease.query, Lease.release}
|
||||||
|
-> Result[List[String], Db.DbError] {
|
||||||
|
let l = pool.acquire()?
|
||||||
|
let rows = l.query(sql)? // 실패하면 lease가 반납되지 않는다
|
||||||
|
l.release()?
|
||||||
|
Ok(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 프로그램 하나. 풀을 열고 쓰고 닫는다.
|
||||||
|
pub fn run(db: Db.Db, url: String, sql: String)
|
||||||
|
effects {Db.pool, Pool.acquire, Pool.close, Lease.query, Lease.release}
|
||||||
|
-> Result[List[String], Db.DbError] {
|
||||||
|
let pool = db.pool(url, 8)?
|
||||||
|
let rows = load_user(pool, sql)? // 실패하면 풀도 안 닫힌다
|
||||||
|
pool.close()?
|
||||||
|
Ok(rows)
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// LRU 캐시 — 자원 판.
|
||||||
|
//
|
||||||
|
// 앞의 두 개밥(파일, 풀)은 정리 지점이 렉시컬이었다. 여기는 아니다.
|
||||||
|
// 축출은 삽입의 부작용으로, 예측할 수 없는 때에 일어난다.
|
||||||
|
//
|
||||||
|
// 이것이 with 결정 전에 알아야 할 자리다 — with는 렉시컬 수명만 다루는데
|
||||||
|
// 축출된 자원은 어느 블록에도 묶이지 않는다.
|
||||||
|
|
||||||
|
import "cool.dev/std/list" as List
|
||||||
|
import "../std-draft/db" as Db
|
||||||
|
|
||||||
|
pub struct Held {
|
||||||
|
key: String,
|
||||||
|
lease: Db.Lease,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lease를 필드로 가지므로 전이적으로 affine이다 — 선언하지 않아도 유도된다.
|
||||||
|
pub struct LeaseCache {
|
||||||
|
cap: Int,
|
||||||
|
held: List[Held],
|
||||||
|
}
|
||||||
|
|
||||||
|
// put은 캐시와 "떨어져 나온 자원"을 함께 돌려줘야 한다. 튜플이 없다.
|
||||||
|
//
|
||||||
|
// struct로 쓰면 막힌다 — 필드 접근은 빌림이라 affine 값을 꺼낼 수 없고,
|
||||||
|
// struct를 분해하는 패턴이 언어에 없다. 열거형은 패턴이 분해하므로 꺼낼 수
|
||||||
|
// 있다. 그래서 "둘을 함께 돌려주기"가 열거형으로만 가능하다.
|
||||||
|
pub enum Put {
|
||||||
|
Kept(LeaseCache),
|
||||||
|
Evicted(LeaseCache, Db.Lease),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn empty(cap: Int) -> LeaseCache {
|
||||||
|
LeaseCache { cap: cap, held: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 넣는다. 넘치면 가장 오래된 것이 떨어져 나온다.
|
||||||
|
//
|
||||||
|
// 떨어져 나온 lease는 호출자가 반납해야 한다. 언어는 그것을 강제하지 못한다 —
|
||||||
|
// Put.evicted를 무시하고 버려도 아무 말이 없다. 누수 허용이므로.
|
||||||
|
pub fn put(own c: LeaseCache, key: String, own l: Db.Lease) -> Put
|
||||||
|
|
||||||
|
// 호출자가 해야 하는 일. 이 함수를 안 부르면 조용히 샌다.
|
||||||
|
pub fn drain(own p: Put) effects {Lease.release} -> LeaseCache {
|
||||||
|
match p {
|
||||||
|
Kept(c) => c,
|
||||||
|
Evicted(c, l) => release_then(l, c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ※ 2판. `own self` 뒤로 db도 pool도 필요 없다 — 반납은 lease 자신이 안다.
|
||||||
|
pub fn release_then(own l: Db.Lease, own c: LeaseCache)
|
||||||
|
effects {Lease.release} -> LeaseCache {
|
||||||
|
let _ = l.release()
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
// 쓰는 쪽. 매 삽입마다 축출을 받아 처리해야 한다.
|
||||||
|
pub fn add(pool: Db.Pool, own c: LeaseCache, key: String)
|
||||||
|
effects {Pool.acquire, Lease.release} -> Result[LeaseCache, Db.DbError] {
|
||||||
|
match pool.acquire() {
|
||||||
|
Err(e) => Err(e),
|
||||||
|
Ok(l) => Ok(drain(put(c, key, l))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(c: LeaseCache) -> Int {
|
||||||
|
List.len(c.held)
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// LRU 캐시 — 값 판.
|
||||||
|
//
|
||||||
|
// 먼저 자원 없이 써서 자료구조 자체의 마찰을 본다. 자원 판은 resources.cool.
|
||||||
|
|
||||||
|
import "cool.dev/std/list" as List
|
||||||
|
|
||||||
|
// ※ Option[Int]의 Option은 내장 타입이라 import가 필요 없다.
|
||||||
|
|
||||||
|
pub copyable struct Entry {
|
||||||
|
key: String,
|
||||||
|
value: Int,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 최근에 쓴 것이 앞. Map이 없어 리스트를 훑는다 — O(n)이다.
|
||||||
|
// LRU의 요점이 O(1)인데 그것을 표현할 수단이 언어에 없다.
|
||||||
|
pub copyable struct Cache {
|
||||||
|
cap: Int,
|
||||||
|
entries: List[Entry],
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn empty(cap: Int) -> Cache {
|
||||||
|
Cache { cap: cap, entries: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// get이 최근성을 바꾸므로 캐시를 새로 돌려줘야 한다.
|
||||||
|
// 그런데 값도 같이 돌려줘야 한다 — 튜플이 없어 struct를 하나 더 만든다.
|
||||||
|
pub copyable struct Got {
|
||||||
|
cache: Cache,
|
||||||
|
value: Option[Int],
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(c: Cache, key: String) -> Got {
|
||||||
|
let hit = List.first(List.filter(c.entries, fn(e) { e.key == key }))
|
||||||
|
match hit {
|
||||||
|
None => Got { cache: c, value: None },
|
||||||
|
Some(e) => Got {
|
||||||
|
cache: Cache { cap: c.cap, entries: touch(c.entries, key, e) },
|
||||||
|
value: Some(e.value),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 찾은 항목을 앞으로 옮긴다.
|
||||||
|
pub fn touch(entries: List[Entry], key: String, e: Entry) -> List[Entry] {
|
||||||
|
List.concat([e], List.filter(entries, fn(x) { !(x.key == key) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn put(c: Cache, key: String, value: Int) -> Cache {
|
||||||
|
let without = List.filter(c.entries, fn(x) { !(x.key == key) })
|
||||||
|
let added = List.concat([Entry { key: key, value: value }], without)
|
||||||
|
Cache { cap: c.cap, entries: evict(added, c.cap) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 넘치면 뒤에서 떨어뜨린다. 값이라 그냥 사라진다 — 자원이면 이야기가 다르다.
|
||||||
|
pub fn evict(entries: List[Entry], cap: Int) -> List[Entry] {
|
||||||
|
if List.len(entries) <= cap {
|
||||||
|
entries
|
||||||
|
} else {
|
||||||
|
evict(drop_last(entries), cap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 마지막 하나를 뺀 리스트. 뒤에서 자르는 함수가 std에 없다.
|
||||||
|
pub fn drop_last(entries: List[Entry]) -> List[Entry] {
|
||||||
|
List.reverse(drop_first(List.reverse(entries)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn drop_first(entries: List[Entry]) -> List[Entry] {
|
||||||
|
List.fold(entries, Dropping { first: true, kept: [] }, fn(own d, e) {
|
||||||
|
if d.first {
|
||||||
|
Dropping { first: false, kept: d.kept }
|
||||||
|
} else {
|
||||||
|
Dropping { first: false, kept: List.push(d.kept, e) }
|
||||||
|
}
|
||||||
|
}).kept
|
||||||
|
}
|
||||||
|
|
||||||
|
// fold에 "첫 원소 건너뛰기"가 없어 운반용 struct를 또 만든다.
|
||||||
|
pub copyable struct Dropping {
|
||||||
|
first: Bool,
|
||||||
|
kept: List[Entry],
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// Mini Shell — 줄을 파이프라인으로 읽는 부분.
|
||||||
|
//
|
||||||
|
// 이 파일에는 자원이 하나도 없다. 일부러 나눴다 — 파싱은 순수하므로
|
||||||
|
// 내장 test로 그 자리에서 증명할 수 있고, 자원이 섞이면 그러지 못한다.
|
||||||
|
// 테스트는 capability를 받지 않으므로 effect-free임이 검사로 증명된다.
|
||||||
|
|
||||||
|
import "cool.dev/std/list" as List
|
||||||
|
import "cool.dev/std/string" as String
|
||||||
|
import "cool.dev/std/option" as Option
|
||||||
|
import "cool.dev/std/result" as Result
|
||||||
|
import "cool.dev/std/test" as Test
|
||||||
|
|
||||||
|
// 명령 하나. program과 args를 나눠 둔다 —
|
||||||
|
// argv[0]에 이름을 겹쳐 두는 관례는 둘 중 뭐가 진짜인지 매번 묻게 만든다.
|
||||||
|
pub copyable struct Command {
|
||||||
|
program: String,
|
||||||
|
args: List[String],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파이프로 이어진 명령들. 비어 있을 수 있다 (빈 줄).
|
||||||
|
pub copyable struct Pipeline {
|
||||||
|
stages: List[Command],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 무엇이 잘못됐는지. 셸은 오류를 사람에게 보여주고 계속 돈다 —
|
||||||
|
// 죽지 않는다. 그래서 crash가 아니라 Result다.
|
||||||
|
pub enum ParseError {
|
||||||
|
// "ls | | wc" — 가운데가 비었다
|
||||||
|
EmptyStage,
|
||||||
|
// "|" 로 시작하거나 끝났다
|
||||||
|
DanglingPipe,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 공백으로 쪼개고 빈 조각을 버린다.
|
||||||
|
// 연속된 공백이 빈 조각을 만들기 때문에 filter가 필요하다.
|
||||||
|
pub fn words(s: String) -> List[String] {
|
||||||
|
List.filter(String.split(String.trim(s), " "), fn(w) { !String.is_empty(w) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 조각 하나를 명령으로. 조각이 비면 EmptyStage다.
|
||||||
|
pub fn stage(s: String) -> Result[Command, ParseError] {
|
||||||
|
let ws = words(s)
|
||||||
|
match List.first(ws) {
|
||||||
|
None => Err(EmptyStage),
|
||||||
|
Some(p) => Ok(Command { program: p, args: List.rest(ws) }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 줄 하나를 파이프라인으로.
|
||||||
|
//
|
||||||
|
// 빈 줄은 오류가 아니다 — 사람이 엔터만 친 것이고, 셸은 아무 일도 하지
|
||||||
|
// 않는다. 그것과 "ls | | wc"는 다른 일이므로 구분한다.
|
||||||
|
pub fn line(raw: String) -> Result[Pipeline, ParseError] {
|
||||||
|
let t = String.trim(raw)
|
||||||
|
if String.is_empty(t) {
|
||||||
|
Ok(Pipeline { stages: [] })
|
||||||
|
} else {
|
||||||
|
stages(String.split(t, "|"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 조각들을 차례로 읽는다. 하나라도 실패하면 전체가 실패한다.
|
||||||
|
//
|
||||||
|
// fold로 접는다. 누적자가 Result라 실패가 그대로 실려간다 — 조기 탈출이
|
||||||
|
// 없어도 뒤 조각이 앞의 실패를 덮지 않는다.
|
||||||
|
pub fn stages(parts: List[String]) -> Result[Pipeline, ParseError] {
|
||||||
|
List.fold(parts, Ok(Pipeline { stages: [] }), fn(own acc, part) {
|
||||||
|
match acc {
|
||||||
|
Err(e) => Err(e),
|
||||||
|
Ok(p) => match stage(part) {
|
||||||
|
Err(e) => Err(e),
|
||||||
|
Ok(c) => Ok(Pipeline { stages: List.push(p.stages, c) }),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 내장 명령인가. cd와 exit은 자식 프로세스로 띄우면 뜻이 없다 —
|
||||||
|
// 자식의 디렉터리를 바꿔 봐야 자식이 죽으면 사라진다.
|
||||||
|
pub fn is_builtin(c: Command) -> Bool {
|
||||||
|
c.program == "cd" || c.program == "exit"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// 테스트
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
test "공백은 몇 칸이든 한 번 자른다" {
|
||||||
|
Test.assert(List.len(words(" ls -l ")) == 2)
|
||||||
|
Test.assert(Option.unwrap_or(List.first(words(" ls -l ")), "") == "ls")
|
||||||
|
}
|
||||||
|
|
||||||
|
test "빈 줄은 아무 단계도 없는 파이프라인이다" {
|
||||||
|
match line(" ") {
|
||||||
|
Err(_) => Test.assert(false),
|
||||||
|
Ok(p) => Test.assert(List.is_empty(p.stages)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "파이프가 단계를 나눈다" {
|
||||||
|
match line("ls -l | grep foo | wc -l") {
|
||||||
|
Err(_) => Test.assert(false),
|
||||||
|
Ok(p) => Test.assert(List.len(p.stages) == 3),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "빈 단계는 오류다" {
|
||||||
|
Test.assert(!Result.is_ok(line("ls | | wc")))
|
||||||
|
Test.assert(!Result.is_ok(line("| ls")))
|
||||||
|
Test.assert(!Result.is_ok(line("ls |")))
|
||||||
|
}
|
||||||
|
|
||||||
|
test "program과 args가 나뉜다" {
|
||||||
|
match stage("grep -i foo") {
|
||||||
|
Err(_) => Test.assert(false),
|
||||||
|
Ok(c) => {
|
||||||
|
Test.assert(c.program == "grep")
|
||||||
|
Test.assert(List.len(c.args) == 2)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
// Mini Shell — 자원을 다루는 부분.
|
||||||
|
//
|
||||||
|
// 앞의 세 개밥(파일, 풀, LRU)이 못 건드린 데를 친다.
|
||||||
|
// 파일: 자원 하나, 렉시컬
|
||||||
|
// 풀: 자원 두 층, 렉시컬
|
||||||
|
// LRU: 정리 지점이 비렉시컬. 다만 자원은 한 종류
|
||||||
|
// 여기: **자원이 여럿이고, 하나가 둘로 갈라져 서로 다른 곳으로 간다**
|
||||||
|
//
|
||||||
|
// 파이프 하나는 끝이 둘이고 각각 다른 자식에게 간다. 부모는 넘긴 뒤
|
||||||
|
// 아무것도 안 남는다. 어느 블록에도 묶이지 않는다 — `with`가 닿지 않는
|
||||||
|
// 모양이 무엇인지 여기서 정확히 보인다.
|
||||||
|
|
||||||
|
import "cool.dev/std/list" as List
|
||||||
|
import "../std-draft/os" as Os
|
||||||
|
import "./parse" as Parse
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// 한 줄 실행
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
// 파이프라인 하나를 돌리고 마지막 단계의 종료 코드를 준다.
|
||||||
|
//
|
||||||
|
// 빈 파이프라인(사람이 엔터만 침)은 0이다. 아무 일도 안 한 것은 성공이다.
|
||||||
|
pub fn run(os: Os.Os, p: Parse.Pipeline)
|
||||||
|
effects {Os.pipe, Os.spawn, Child.wait, ReadFd.close, WriteFd.close}
|
||||||
|
-> Result[Int, Os.OsError] {
|
||||||
|
if List.is_empty(p.stages) {
|
||||||
|
Ok(0)
|
||||||
|
} else {
|
||||||
|
from(os, p.stages, InheritIn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 단계 목록을 앞에서부터 실행한다. stdin은 앞 단계가 정해 준다.
|
||||||
|
//
|
||||||
|
// 재귀가 필요하다. 자식을 리스트에 모아 두고 나중에 한꺼번에 거두는 쪽이
|
||||||
|
// 자연스러운데 그렇게 못 한다 — 아래 "리스트에 넣으면 못 꺼낸다" 참고.
|
||||||
|
// 그래서 "뒤를 먼저 다 띄우고, 돌아오면서 자기 자식을 거둔다"가 된다.
|
||||||
|
pub fn from(os: Os.Os, stages: List[Parse.Command], own stdin: Os.Stdin)
|
||||||
|
effects {Os.pipe, Os.spawn, Child.wait, ReadFd.close, WriteFd.close}
|
||||||
|
-> Result[Int, Os.OsError] {
|
||||||
|
match List.first(stages) {
|
||||||
|
// 여기 오면 stdin이 샌다. 언어는 말하지 않는다 (D3).
|
||||||
|
None => Ok(0),
|
||||||
|
Some(c) => {
|
||||||
|
let tail = List.rest(stages)
|
||||||
|
if List.is_empty(tail) {
|
||||||
|
last(os, c, stdin)
|
||||||
|
} else {
|
||||||
|
middle(os, c, stdin, tail)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 마지막 단계. 출력은 부모 것을 그대로 쓴다.
|
||||||
|
pub fn last(os: Os.Os, c: Parse.Command, own stdin: Os.Stdin)
|
||||||
|
effects {Os.spawn, Child.wait} -> Result[Int, Os.OsError] {
|
||||||
|
match os.spawn(c.program, c.args, stdin, InheritOut) {
|
||||||
|
Err(e) => Err(e),
|
||||||
|
Ok(child) => child.wait(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 가운데 단계. 파이프를 만들어 쓰는 끝을 자식에게 주고,
|
||||||
|
// 읽는 끝을 다음 단계에 넘긴다.
|
||||||
|
//
|
||||||
|
// 이 함수가 이 개밥 먹기의 본체다. 자원 셋(stdin, 읽는 끝, 쓰는 끝)이
|
||||||
|
// 서로 다른 곳으로 가고, 실패하면 셋의 상태가 제각각이다.
|
||||||
|
pub fn middle(
|
||||||
|
os: Os.Os,
|
||||||
|
c: Parse.Command,
|
||||||
|
own stdin: Os.Stdin,
|
||||||
|
rest: List[Parse.Command],
|
||||||
|
) effects {Os.pipe, Os.spawn, Child.wait, ReadFd.close, WriteFd.close}
|
||||||
|
-> Result[Int, Os.OsError] {
|
||||||
|
match os.pipe() {
|
||||||
|
// stdin이 샌다
|
||||||
|
Err(e) => Err(e),
|
||||||
|
Ok(p) => match p {
|
||||||
|
Ends(r, w) => match os.spawn(c.program, c.args, stdin, ToPipe(w)) {
|
||||||
|
// 여기서는 r이 샌다. w는 spawn이 가져갔는지 아닌지
|
||||||
|
// 시그니처만 봐서는 알 수 없다 — 아래 발견 참고.
|
||||||
|
Err(e) => Err(e),
|
||||||
|
// 자식이 떴다. 부모는 쓰는 끝을 이미 넘겼고, 읽는 끝을
|
||||||
|
// 다음 단계에 넘긴다. 부모에게는 아무것도 안 남는다.
|
||||||
|
Ok(child) => reap(child, from(os, rest, FromPipe(r))),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 자식을 거두고 뒤쪽의 결과를 돌려준다.
|
||||||
|
//
|
||||||
|
// 뒤쪽 결과가 앞선다 — 파이프라인의 종료 코드는 마지막 단계의 것이다.
|
||||||
|
// 앞 단계가 죽어도 셸은 마지막 것을 보고한다.
|
||||||
|
//
|
||||||
|
// 거두기 실패는 일부러 버린다. 자식이 이미 사라진 것은 우리가 할 일이
|
||||||
|
// 없고, 뒤쪽 결과가 사람이 알고 싶은 것이다.
|
||||||
|
pub fn reap(own child: Os.Child, r: Result[Int, Os.OsError])
|
||||||
|
effects {Child.wait} -> Result[Int, Os.OsError] {
|
||||||
|
let _ = child.wait()
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// 되풀이
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
// 읽고, 읽어서 나눈 것을 돌리고, 다시 읽는다.
|
||||||
|
//
|
||||||
|
// 내장 명령은 자식으로 띄우지 않는다. cd를 자식에서 하면 자식의 디렉터리가
|
||||||
|
// 바뀌고 자식은 곧 죽는다 — 아무 일도 안 일어난 것과 같다.
|
||||||
|
pub fn once(os: Os.Os, line: String)
|
||||||
|
effects {Os.pipe, Os.spawn, Os.chdir, Child.wait,
|
||||||
|
ReadFd.close, WriteFd.close}
|
||||||
|
-> Result[Int, Os.OsError] {
|
||||||
|
match Parse.line(line) {
|
||||||
|
// 파싱 실패는 셸을 죽이지 않는다. 사람이 오타를 낸 것뿐이다.
|
||||||
|
Err(_) => Ok(2),
|
||||||
|
Ok(p) => match List.first(p.stages) {
|
||||||
|
None => Ok(0),
|
||||||
|
Some(head) => {
|
||||||
|
if Parse.is_builtin(head) && List.len(p.stages) == 1 {
|
||||||
|
builtin(os, head)
|
||||||
|
} else {
|
||||||
|
run(os, p)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 내장 명령. cd만 실제로 뭔가 한다.
|
||||||
|
//
|
||||||
|
// exit은 여기서 처리할 수 없다 — 되풀이를 멈추는 일은 부르는 쪽의
|
||||||
|
// 제어 흐름이지 이 함수의 결과값이 아니다. 종료 코드로 신호를 보낸다.
|
||||||
|
pub fn builtin(os: Os.Os, c: Parse.Command)
|
||||||
|
effects {Os.chdir} -> Result[Int, Os.OsError] {
|
||||||
|
if c.program == "exit" {
|
||||||
|
Ok(0 - 1)
|
||||||
|
} else {
|
||||||
|
match List.first(c.args) {
|
||||||
|
None => Ok(0),
|
||||||
|
Some(dir) => match os.chdir(dir) {
|
||||||
|
Err(e) => Err(e),
|
||||||
|
Ok(_) => Ok(0),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// 데이터베이스 연결 풀 — 개밥 먹기용 초안.
|
||||||
|
//
|
||||||
|
// 세 층이 있고 수명을 관리하는 주체가 각각 다르다.
|
||||||
|
// Conn 실제 연결. 풀이 소유한다. 사용자는 존재도 모른다
|
||||||
|
// Pool 연결 묶음. 프로그램 수명. 사용자가 만들고 닫는다
|
||||||
|
// Lease 빌린 한 자리. 요청 하나 수명. 반드시 반납해야 한다
|
||||||
|
//
|
||||||
|
// 사용자가 관리하는 것은 연결의 수명이 아니라 lease의 수명이다.
|
||||||
|
// 그래서 반납은 "파괴"가 아니다 — 연결은 풀로 돌아간다.
|
||||||
|
//
|
||||||
|
// ※ 2판. `own self`가 생긴 뒤 다시 썼다. 전에는 정리가 전부 Db의 메서드라
|
||||||
|
// 반납 한 번에 풀과 권한을 같이 들고 다녀야 했다. 이제 자원이 자기
|
||||||
|
// 정리를 스스로 선언한다.
|
||||||
|
|
||||||
|
// ※ List[String]의 List는 내장 타입이라 import가 필요 없다. 모듈 List를
|
||||||
|
// 가져오는 것은 그 타입에 딸린 함수(len, map...)를 쓸 때뿐이다.
|
||||||
|
|
||||||
|
pub enum DbError {
|
||||||
|
// 연결 자체가 안 된다
|
||||||
|
Unreachable(String),
|
||||||
|
// 풀이 가득 찼고 기다릴 수 없다. 호출자가 대처할 수 있다 — 재시도든 포기든
|
||||||
|
Exhausted,
|
||||||
|
// 질의가 틀렸다. 프로그램의 결함에 가깝지만 런타임 값에서 오므로 Result다
|
||||||
|
BadQuery(String),
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 빌린 한 자리. 반드시 반납해야 한다.
|
||||||
|
// ※ 언어는 아직 그 "반드시"를 강제하지 못한다 (D3). `with`가 그 자리다.
|
||||||
|
pub capability Lease {
|
||||||
|
// 질의한다. 수신자를 빌린다 — 한 lease로 여러 번 질의할 수 있다.
|
||||||
|
fn query(sql: String) effects {Lease.query} -> Result[List[String], DbError]
|
||||||
|
|
||||||
|
// 반납한다. 연결은 풀로 돌아간다 — 파괴가 아니다.
|
||||||
|
// 수신자를 소비하므로 두 번 반납할 수 없다. 그리고 이 시그니처에는
|
||||||
|
// 풀도 Db 권한도 없다 — 반납에 필요한 것은 lease 자신뿐이다.
|
||||||
|
// 그것이 `with`가 성립하기 위한 조건이다 (D9).
|
||||||
|
fn release(own self) effects {Lease.release} -> Result[Unit, DbError]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 풀. capability이므로 affine이다 — 복제되지 않는다.
|
||||||
|
pub capability Pool {
|
||||||
|
// 한 자리를 빌린다. 없으면 Exhausted — 기다리지 않는다.
|
||||||
|
// 기다리는 형태(timeout)는 취소를 정해야 해서 미룬다.
|
||||||
|
fn acquire() effects {Pool.acquire} -> Result[Lease, DbError]
|
||||||
|
|
||||||
|
// 풀을 닫는다. 안에 있는 연결이 전부 정리된다.
|
||||||
|
// 아직 빌려나간 lease가 있으면? — 아래 발견 참고.
|
||||||
|
fn close(own self) effects {Pool.close} -> Result[Unit, DbError]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 데이터베이스에 손댈 권한. 풀을 만들 수 있는 유일한 출처다.
|
||||||
|
pub capability Db {
|
||||||
|
// 풀을 연다. size는 최대 동시 연결 수.
|
||||||
|
fn pool(url: String, size: Int) effects {Db.pool} -> Result[Pool, DbError]
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// 파일 시스템 — 개밥 먹기용 초안.
|
||||||
|
//
|
||||||
|
// 이 파일은 구현이 아니라 계약이다. 런타임이 구현한다고 가정하고, coollang이
|
||||||
|
// 이 일을 표현할 수 있는지만 본다. 실행은 안 되지만 타입·effect·capability·
|
||||||
|
// 소유권 검사는 전부 진짜로 돈다.
|
||||||
|
//
|
||||||
|
// 규율: 여기 적는 것은 지금 런타임이 가진 권한으로 구현 가능해야 하고,
|
||||||
|
// effect를 전부 선언해야 하며, 없는 언어 기능에 기대면 안 된다. 없는 기능이
|
||||||
|
// 필요하다는 게 드러나면 그것이 발견이지 지름길이 아니다.
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// 실패는 무엇인가
|
||||||
|
//
|
||||||
|
// 여기서 Result와 crash의 선을 긋는다. 기준은 "호출자가 대처할 수 있는가"다.
|
||||||
|
// 대처할 수 있다 → Result
|
||||||
|
// 프로그램이 틀렸다 → crash
|
||||||
|
//
|
||||||
|
// 그래서 아래는 전부 Result다. 파일이 없는 것도, 권한이 없는 것도, 디스크가
|
||||||
|
// 가득 찬 것도 프로그램의 결함이 아니다 — 세상의 상태다.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub enum IoError {
|
||||||
|
NotFound(String),
|
||||||
|
Denied(String),
|
||||||
|
Exists(String),
|
||||||
|
NoSpace(String),
|
||||||
|
// 나머지. 런타임이 분류하지 못한 것들
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 열린 쓰기 핸들.
|
||||||
|
//
|
||||||
|
// capability로 선언하는 이유가 둘이다.
|
||||||
|
// 1. 이것은 실제로 권한이다 — 이 파일에 쓸 수 있는 권한
|
||||||
|
// 2. capability는 affinity의 뿌리이므로 복제되지 않는다
|
||||||
|
//
|
||||||
|
// ※ 2판. `own self`가 생긴 뒤 다시 썼다. 1판에서는 핸들에 메서드가 하나도
|
||||||
|
// 없었다 — close가 핸들을 소비해야 하는데 수신자를 소비할 방법이 없어서
|
||||||
|
// 전부 Fs의 메서드로 밀어냈다. 이제 핸들이 자기 메서드를 갖는다.
|
||||||
|
//
|
||||||
|
// 그러면서 권한의 모양이 바뀌었다. 쓰기·동기화·닫기에 더 이상 Fs가 필요
|
||||||
|
// 없다. 핸들만 넘기면 그 파일에 쓸 권한만 넘어가고 파일 시스템 전체 권한은
|
||||||
|
// 넘어가지 않는다 — 권한이 좁아진 채로 전달된다. 1판에서는 쓰게 하려면
|
||||||
|
// Fs를 통째로 줘야 했다.
|
||||||
|
pub capability WriteFile {
|
||||||
|
// 핸들을 빌린다. 여러 번 쓸 수 있다.
|
||||||
|
fn write(s: String) effects {WriteFile.write} -> Result[Unit, IoError]
|
||||||
|
|
||||||
|
// 디스크까지 내려간다. 이게 없으면 rename이 원자적이어도 내용이 없을 수 있다.
|
||||||
|
fn sync() effects {WriteFile.sync} -> Result[Unit, IoError]
|
||||||
|
|
||||||
|
// 수신자를 소비한다. 두 번 닫을 수 없다 — own self가 그것을 강제한다.
|
||||||
|
// 이 시그니처에 Fs가 없다는 것이 요점이다. 정리에 필요한 것이 자원
|
||||||
|
// 자신뿐이어야 `with`가 자원만 보고 무엇을 부를지 알 수 있다 (D9).
|
||||||
|
fn close(own self) effects {WriteFile.close} -> Result[Unit, IoError]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파일 시스템에 손댈 권한. 이름으로 하는 일이 여기 남는다 —
|
||||||
|
// 경로는 파일 시스템 전체에 대한 이름이므로 전체 권한이 필요하다.
|
||||||
|
pub capability Fs {
|
||||||
|
// 새로 만든다. 이미 있으면 자른다.
|
||||||
|
fn create(path: String) effects {Fs.create} -> Result[WriteFile, IoError]
|
||||||
|
|
||||||
|
// 같은 파일 시스템 안에서 원자적이다.
|
||||||
|
fn rename(from: String, to: String) effects {Fs.rename} -> Result[Unit, IoError]
|
||||||
|
|
||||||
|
fn remove(path: String) effects {Fs.remove} -> Result[Unit, IoError]
|
||||||
|
|
||||||
|
fn read(path: String) effects {Fs.read} -> Result[String, IoError]
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
// 프로세스와 파이프 — 개밥 먹기용 초안.
|
||||||
|
//
|
||||||
|
// 이 파일은 구현이 아니라 계약이다. Mini Shell을 쓰기 위해 필요한 최소한만
|
||||||
|
// 적는다. 규율은 fs.cool과 같다 — 없는 언어 기능에 기대면 그것이 발견이다.
|
||||||
|
//
|
||||||
|
// 여기서 처음 나오는 것: **자원의 수명이 렉시컬이 아니다.**
|
||||||
|
// 파이프 하나는 끝이 둘이고, 각각 다른 자식에게 간다. 부모는 넘긴 뒤
|
||||||
|
// 자기 몫을 닫아야 한다. 어느 블록에도 묶이지 않는다.
|
||||||
|
|
||||||
|
pub enum OsError {
|
||||||
|
NotFound(String),
|
||||||
|
Denied(String),
|
||||||
|
// fd가 동났다 / 프로세스가 동났다. 세상의 상태지 프로그램의 결함이 아니다
|
||||||
|
Exhausted(String),
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 열린 파일 서술자. 읽는 쪽과 쓰는 쪽을 타입으로 나눈다 —
|
||||||
|
// 읽는 끝에 쓰는 것은 프로그램의 결함이고, 결함은 타입이 막는 것이 낫다.
|
||||||
|
pub capability ReadFd {
|
||||||
|
fn close(own self) effects {ReadFd.close} -> Result[Unit, OsError]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub capability WriteFd {
|
||||||
|
fn close(own self) effects {WriteFd.close} -> Result[Unit, OsError]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파이프. 만들면 끝이 둘 생긴다.
|
||||||
|
//
|
||||||
|
// struct로 담으면 필드에서 꺼낼 수 없다 (부분 move가 없다). 열거형으로
|
||||||
|
// 감싸면 패턴이 분해하므로 꺼낼 수 있다. 그래서 "둘을 함께 돌려주기"가
|
||||||
|
// 열거형으로만 된다 — LRU에서 나온 것과 같은 자리다 (D11).
|
||||||
|
pub enum Pipe {
|
||||||
|
Ends(ReadFd, WriteFd),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 살아 있는 자식 프로세스. 반드시 거둬야 한다 — 안 거두면 좀비가 된다.
|
||||||
|
pub capability Child {
|
||||||
|
// 끝날 때까지 기다리고 종료 코드를 준다. 수신자를 소비한다 —
|
||||||
|
// 한 번 거둔 자식을 다시 거둘 수 없다.
|
||||||
|
fn wait(own self) effects {Child.wait} -> Result[Int, OsError]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 자식에게 물려줄 표준 입출력. 무엇을 물려줄지는 세 가지뿐이다.
|
||||||
|
//
|
||||||
|
// 물려주는 fd는 소비된다 — 자식에게 넘어가고 부모는 더 못 쓴다.
|
||||||
|
// 그것이 own이 시그니처에 있는 이유다.
|
||||||
|
//
|
||||||
|
// ※ 발견: variant 이름이 모듈 전역이라 Stdin.Inherit와 Stdout.Inherit가
|
||||||
|
// 충돌한다. 그래서 InheritIn / InheritOut이 됐다 — 뜻이 아니라 언어의
|
||||||
|
// 제약이 고른 이름이다. 철학 6이 지지 않아야 할 방식으로 진 자리다.
|
||||||
|
pub enum Stdin {
|
||||||
|
// 부모 것을 그대로
|
||||||
|
InheritIn,
|
||||||
|
// 파이프의 읽는 끝
|
||||||
|
FromPipe(ReadFd),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Stdout {
|
||||||
|
InheritOut,
|
||||||
|
ToPipe(WriteFd),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 프로세스를 만들 권한.
|
||||||
|
pub capability Os {
|
||||||
|
fn pipe() effects {Os.pipe} -> Result[Pipe, OsError]
|
||||||
|
|
||||||
|
// 프로그램을 띄운다. argv[0]은 프로그램 이름이 아니라 인자다 —
|
||||||
|
// 이름은 따로 받는다. 관례로 겹쳐 두면 둘 중 뭐가 진짜인지 매번 묻게 된다.
|
||||||
|
fn spawn(
|
||||||
|
program: String,
|
||||||
|
args: List[String],
|
||||||
|
own stdin: Stdin,
|
||||||
|
own stdout: Stdout,
|
||||||
|
) effects {Os.spawn} -> Result[Child, OsError]
|
||||||
|
|
||||||
|
// 현재 디렉터리를 바꾼다. 프로세스 전역 상태이므로 권한이 필요하다.
|
||||||
|
fn chdir(path: String) effects {Os.chdir} -> Result[Unit, OsError]
|
||||||
|
|
||||||
|
fn cwd() effects {Os.cwd} -> String
|
||||||
|
}
|
||||||
|
|
||||||
|
// 줄 단위 입력. 끝나면 None.
|
||||||
|
pub capability Stdin_ {
|
||||||
|
fn read_line() effects {Stdin_.read_line} -> Result[Option[String], OsError]
|
||||||
|
}
|
||||||
+58
-10
@@ -11,10 +11,18 @@ type eff_atom = Eff_var of string | Eff_set of eff_name list
|
|||||||
type eff_result = eff_atom list (* 합집합. 길이 1이면 단일 *)
|
type eff_result = eff_atom list (* 합집합. 길이 1이면 단일 *)
|
||||||
|
|
||||||
type ty =
|
type ty =
|
||||||
| T_named of { name : string; args : targ list; pos : pos }
|
(* modl: 다른 모듈의 타입은 별칭으로 한정한다 (Shapes.Shape).
|
||||||
|
한정하지 않으면 이 모듈의 이름이다 — 암묵적으로 끌어오지 않는다. *)
|
||||||
|
| T_named of {
|
||||||
|
modl : string option;
|
||||||
|
name : string;
|
||||||
|
args : targ list;
|
||||||
|
pos : pos;
|
||||||
|
}
|
||||||
| T_fn of {
|
| T_fn of {
|
||||||
affine : bool;
|
affine : bool;
|
||||||
params : ty list;
|
(* 파라미터마다 소유권 표시. 무표기는 빌림 *)
|
||||||
|
params : fn_param_ty list;
|
||||||
eff : eff_atom option;
|
eff : eff_atom option;
|
||||||
ret : ty option;
|
ret : ty option;
|
||||||
pos : pos;
|
pos : pos;
|
||||||
@@ -22,13 +30,19 @@ type ty =
|
|||||||
|
|
||||||
(* 제네릭 인자는 타입 또는 effect다. 맨 이름은 둘 다일 수 있으므로
|
(* 제네릭 인자는 타입 또는 effect다. 맨 이름은 둘 다일 수 있으므로
|
||||||
파서는 타입으로 읽고 이름 해소가 판정한다. *)
|
파서는 타입으로 읽고 이름 해소가 판정한다. *)
|
||||||
|
and fn_param_ty = { pt_own : bool; pt_ty : ty }
|
||||||
and targ = TA_ty of ty | TA_eff of eff_atom
|
and targ = TA_ty of ty | TA_eff of eff_atom
|
||||||
|
|
||||||
type pattern =
|
type pattern =
|
||||||
| P_wild of pos
|
| P_wild of pos
|
||||||
| P_lit of lit * pos
|
| P_lit of lit * pos
|
||||||
| P_bind of string * pos
|
| P_bind of string * pos
|
||||||
| P_ctor of { name : string; args : pattern list; pos : pos }
|
| P_ctor of {
|
||||||
|
modl : string option;
|
||||||
|
name : string;
|
||||||
|
args : pattern list;
|
||||||
|
pos : pos;
|
||||||
|
}
|
||||||
|
|
||||||
type unop = U_not | U_neg
|
type unop = U_not | U_neg
|
||||||
|
|
||||||
@@ -62,16 +76,21 @@ type expr =
|
|||||||
| E_inst of { callee : expr; args : targ list; pos : pos }
|
| E_inst of { callee : expr; args : targ list; pos : pos }
|
||||||
| E_try of { inner : expr; pos : pos }
|
| E_try of { inner : expr; pos : pos }
|
||||||
| E_unary of { op : unop; operand : expr; pos : pos }
|
| E_unary of { op : unop; operand : expr; pos : pos }
|
||||||
|
(* 복구 불가능한 실패. 타입은 Never — 돌아오지 않으므로 어떤 자리에도 놓인다 *)
|
||||||
|
| E_crash of { msg : expr; pos : pos }
|
||||||
| E_binary of { op : binop; lhs : expr; rhs : expr; pos : pos }
|
| E_binary of { op : binop; lhs : expr; rhs : expr; pos : pos }
|
||||||
|
|
||||||
and closure = {
|
and closure = {
|
||||||
cl_params : (string * ty option) list;
|
(* (own, 이름, 타입). 타입은 기대 타입에서 읽어오지만 소유권은 리터럴이
|
||||||
|
스스로 적는다 — move 검사가 타입을 모르기 때문이다 *)
|
||||||
|
cl_params : cl_param list;
|
||||||
cl_eff : eff_atom option;
|
cl_eff : eff_atom option;
|
||||||
cl_ret : ty option;
|
cl_ret : ty option;
|
||||||
cl_body : block;
|
cl_body : block;
|
||||||
cl_pos : pos;
|
cl_pos : pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
and cl_param = { cp_own : bool; cp_name : string; cp_ty : ty option }
|
||||||
and arm = { arm_pat : pattern; arm_body : expr; arm_pos : pos }
|
and arm = { arm_pat : pattern; arm_body : expr; arm_pos : pos }
|
||||||
and block = { stmts : stmt list; block_pos : pos }
|
and block = { stmts : stmt list; block_pos : pos }
|
||||||
|
|
||||||
@@ -100,6 +119,14 @@ type param = {
|
|||||||
type fn_decl = {
|
type fn_decl = {
|
||||||
fn_name : string;
|
fn_name : string;
|
||||||
fn_gen : gen_param list;
|
fn_gen : gen_param list;
|
||||||
|
(* 수신자를 소비하는가. capability 메서드에만 있고, 소스에는 `own self`로
|
||||||
|
적는다. 빌리는 것이 기본이므로 빌림에는 적을 말이 없다 — 뜻이 있는
|
||||||
|
쪽에만 표기가 있다.
|
||||||
|
|
||||||
|
이것이 없으면 자원이 자기 정리를 스스로 선언할 수 없고, 정리에 항상
|
||||||
|
바깥 권한(Fs, Db)이 따라붙는다. 그러면 `with`가 자원만 보고 무엇을
|
||||||
|
불러야 할지 알 수 없다 (dogfoods/FINDINGS D9). *)
|
||||||
|
fn_self : bool;
|
||||||
fn_params : param list;
|
fn_params : param list;
|
||||||
fn_eff : eff_result option;
|
fn_eff : eff_result option;
|
||||||
fn_ret : ty option;
|
fn_ret : ty option;
|
||||||
@@ -136,6 +163,9 @@ type item =
|
|||||||
pos : pos;
|
pos : pos;
|
||||||
}
|
}
|
||||||
| I_const of { pub : bool; name : string; ty : ty; value : expr; pos : pos }
|
| I_const of { pub : bool; name : string; ty : ty; value : expr; pos : pos }
|
||||||
|
(* 테스트. 파라미터가 없으므로 capability를 받을 수 없고, 그래서
|
||||||
|
effect-free임이 증명된다 — 관례가 아니라 검사다 *)
|
||||||
|
| I_test of { name : string; body : block; pos : pos }
|
||||||
|
|
||||||
type modul = { items : item list }
|
type modul = { items : item list }
|
||||||
|
|
||||||
@@ -164,7 +194,8 @@ let buf_eff_atom b = function
|
|||||||
names
|
names
|
||||||
|
|
||||||
let rec buf_ty b = function
|
let rec buf_ty b = function
|
||||||
| T_named { name; args; _ } ->
|
| T_named { modl; name; args; _ } ->
|
||||||
|
let name = match modl with None -> name | Some m -> m ^ "." ^ name in
|
||||||
if args = [] then Buffer.add_string b name
|
if args = [] then Buffer.add_string b name
|
||||||
else (
|
else (
|
||||||
Buffer.add_string b ("(" ^ name);
|
Buffer.add_string b ("(" ^ name);
|
||||||
@@ -176,7 +207,11 @@ let rec buf_ty b = function
|
|||||||
Buffer.add_char b ')')
|
Buffer.add_char b ')')
|
||||||
| T_fn { affine; params; eff; ret; _ } ->
|
| T_fn { affine; params; eff; ret; _ } ->
|
||||||
Buffer.add_string b (if affine then "(affine-fn (" else "(fn (");
|
Buffer.add_string b (if affine then "(affine-fn (" else "(fn (");
|
||||||
buf_list b (buf_ty b) " " params;
|
buf_list b
|
||||||
|
(fun (p : fn_param_ty) ->
|
||||||
|
if p.pt_own then Buffer.add_string b "own ";
|
||||||
|
buf_ty b p.pt_ty)
|
||||||
|
" " params;
|
||||||
Buffer.add_char b ')';
|
Buffer.add_char b ')';
|
||||||
(match eff with
|
(match eff with
|
||||||
| None -> ()
|
| None -> ()
|
||||||
@@ -202,7 +237,8 @@ let rec buf_pattern b = function
|
|||||||
| P_wild _ -> Buffer.add_char b '_'
|
| P_wild _ -> Buffer.add_char b '_'
|
||||||
| P_lit (l, _) -> buf_lit b l
|
| P_lit (l, _) -> buf_lit b l
|
||||||
| P_bind (n, _) -> Buffer.add_string b n
|
| P_bind (n, _) -> Buffer.add_string b n
|
||||||
| P_ctor { name; args; _ } ->
|
| P_ctor { modl; name; args; _ } ->
|
||||||
|
let name = match modl with None -> name | Some m -> m ^ "." ^ name in
|
||||||
Buffer.add_string b ("(" ^ name);
|
Buffer.add_string b ("(" ^ name);
|
||||||
List.iter
|
List.iter
|
||||||
(fun p ->
|
(fun p ->
|
||||||
@@ -230,6 +266,10 @@ let binop_name = function
|
|||||||
|
|
||||||
let rec buf_expr b = function
|
let rec buf_expr b = function
|
||||||
| E_lit (l, _) -> buf_lit b l
|
| E_lit (l, _) -> buf_lit b l
|
||||||
|
| E_crash { msg; _ } ->
|
||||||
|
Buffer.add_string b "(crash ";
|
||||||
|
buf_expr b msg;
|
||||||
|
Buffer.add_char b ')'
|
||||||
| E_ident (n, _) -> Buffer.add_string b n
|
| E_ident (n, _) -> Buffer.add_string b n
|
||||||
| E_list (xs, _) ->
|
| E_list (xs, _) ->
|
||||||
Buffer.add_string b "(list";
|
Buffer.add_string b "(list";
|
||||||
@@ -251,9 +291,10 @@ let rec buf_expr b = function
|
|||||||
| E_closure c ->
|
| E_closure c ->
|
||||||
Buffer.add_string b "(closure (";
|
Buffer.add_string b "(closure (";
|
||||||
buf_list b
|
buf_list b
|
||||||
(fun (n, t) ->
|
(fun (p : cl_param) ->
|
||||||
Buffer.add_string b n;
|
if p.cp_own then Buffer.add_string b "own ";
|
||||||
match t with
|
Buffer.add_string b p.cp_name;
|
||||||
|
match p.cp_ty with
|
||||||
| None -> ()
|
| None -> ()
|
||||||
| Some t ->
|
| Some t ->
|
||||||
Buffer.add_char b ':';
|
Buffer.add_char b ':';
|
||||||
@@ -377,6 +418,9 @@ let buf_gen b gen =
|
|||||||
let buf_fn b (d : fn_decl) =
|
let buf_fn b (d : fn_decl) =
|
||||||
Buffer.add_string b ("(fn " ^ d.fn_name);
|
Buffer.add_string b ("(fn " ^ d.fn_name);
|
||||||
buf_gen b d.fn_gen;
|
buf_gen b d.fn_gen;
|
||||||
|
(* 해시에 들어간다. 수신자를 소비하는지가 바뀌면 호출하는 쪽의 move 검사
|
||||||
|
결과가 바뀐다 — 표면이다 *)
|
||||||
|
if d.fn_self then Buffer.add_string b " own-self";
|
||||||
Buffer.add_string b " (";
|
Buffer.add_string b " (";
|
||||||
buf_list b
|
buf_list b
|
||||||
(fun p ->
|
(fun p ->
|
||||||
@@ -454,6 +498,10 @@ let buf_item b = function
|
|||||||
Buffer.add_char b ' ';
|
Buffer.add_char b ' ';
|
||||||
buf_expr b value;
|
buf_expr b value;
|
||||||
Buffer.add_char b ')'
|
Buffer.add_char b ')'
|
||||||
|
| I_test { name; body; _ } ->
|
||||||
|
Buffer.add_string b ("(test \"" ^ name ^ "\" ");
|
||||||
|
buf_block b body;
|
||||||
|
Buffer.add_char b ')'
|
||||||
|
|
||||||
let show_item item =
|
let show_item item =
|
||||||
let b = Buffer.create 256 in
|
let b = Buffer.create 256 in
|
||||||
|
|||||||
+25
-32
@@ -1,8 +1,10 @@
|
|||||||
(* v0 파이프라인.
|
(* v0 파이프라인.
|
||||||
parse -> name resolution -> type check -> effect/capability check
|
parse -> name resolution -> type check -> effect/capability check
|
||||||
-> interface artifact + hash -> (cool run 시) 얇은 typed IR -> interpreter
|
-> interface artifact + hash -> (coolc run 시) 얇은 typed IR -> interpreter
|
||||||
|
|
||||||
현재 구현된 단계: 어휘 분석, 구문 분석. *)
|
이 파일은 단일 파일 도구(tokens/ast/deps)만 남았다. 모듈 그래프를 다루는
|
||||||
|
check와 run은 Session이 소유한다 — import를 따라가야 하는 순간부터
|
||||||
|
"파일 하나"는 더 이상 단위가 아니다. *)
|
||||||
|
|
||||||
type error = { file : string; line : int; col : int; message : string }
|
type error = { file : string; line : int; col : int; message : string }
|
||||||
|
|
||||||
@@ -69,19 +71,27 @@ let typecheck (file : string) : (unit, error list) result =
|
|||||||
{ file; line = e.pos.line; col = e.pos.col; message = e.msg })
|
{ file; line = e.pos.line; col = e.pos.col; message = e.msg })
|
||||||
rerrors)
|
rerrors)
|
||||||
| [] -> (
|
| [] -> (
|
||||||
match Typecheck.check m with
|
let terrors =
|
||||||
| [] -> Ok ()
|
List.map
|
||||||
| terrors ->
|
|
||||||
Error
|
|
||||||
(List.map
|
|
||||||
(fun (e : Typecheck.error) ->
|
(fun (e : Typecheck.error) ->
|
||||||
{
|
{ file; line = e.pos.line; col = e.pos.col; message = e.msg })
|
||||||
file;
|
(Typecheck.check m)
|
||||||
line = e.pos.line;
|
in
|
||||||
col = e.pos.col;
|
(* move/affinity는 타입·effect와 달리 별도 순회다. 소유하는 성질이
|
||||||
message = e.msg;
|
다르고 해소를 공유할 지점도 없기 때문이다. *)
|
||||||
})
|
let merrors =
|
||||||
terrors)))
|
List.map
|
||||||
|
(fun (e : Move.error) ->
|
||||||
|
{ file; line = e.pos.line; col = e.pos.col; message = e.msg })
|
||||||
|
(Move.check m)
|
||||||
|
in
|
||||||
|
match
|
||||||
|
List.sort
|
||||||
|
(fun a b -> compare (a.line, a.col) (b.line, b.col))
|
||||||
|
(terrors @ merrors)
|
||||||
|
with
|
||||||
|
| [] -> Ok ()
|
||||||
|
| errors -> Error errors))
|
||||||
|
|
||||||
let check (files : string list) : (unit, error list) result =
|
let check (files : string list) : (unit, error list) result =
|
||||||
match files with
|
match files with
|
||||||
@@ -94,21 +104,4 @@ let check (files : string list) : (unit, error list) result =
|
|||||||
files
|
files
|
||||||
|> List.concat
|
|> List.concat
|
||||||
in
|
in
|
||||||
if errors <> [] then Error errors
|
if errors <> [] then Error errors else Ok ()
|
||||||
else
|
|
||||||
(* 타입 검사는 통과했다. 통과했다고 말하지 않는다 — 파이프라인의
|
|
||||||
나머지가 아직 없으므로 검사되지 않은 것이다. *)
|
|
||||||
Error
|
|
||||||
(List.map
|
|
||||||
(fun f ->
|
|
||||||
{
|
|
||||||
file = f;
|
|
||||||
line = 0;
|
|
||||||
col = 0;
|
|
||||||
message =
|
|
||||||
"타입 검사까지 통과. effect/capability 검사와 move 검사가 아직 구현되지 않았습니다";
|
|
||||||
})
|
|
||||||
files)
|
|
||||||
|
|
||||||
let run (file : string) : (unit, error list) result =
|
|
||||||
Error [ { file; line = 0; col = 0; message = "interpreter가 아직 구현되지 않았습니다" } ]
|
|
||||||
|
|||||||
+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)
|
||||||
+203
@@ -0,0 +1,203 @@
|
|||||||
|
(* match exhaustiveness와 도달 불가 팔 검사 (Maranget의 usefulness 알고리즘).
|
||||||
|
|
||||||
|
철학 1의 대표 항목이다. 그리고 interface hash가 enum 정의 본문을 입력으로
|
||||||
|
삼는 이유이기도 하다 — upstream에 variant가 하나 늘면 downstream의 match가
|
||||||
|
깨져야 하는데, 이 검사가 없으면 깨질 것이 없다.
|
||||||
|
|
||||||
|
생성자 집합을 알 수 없는 타입(외부 타입, 미지수)은 검사하지 않는다.
|
||||||
|
모르는 것을 위반이라고 말하지 않는다. *)
|
||||||
|
|
||||||
|
module T = Types
|
||||||
|
|
||||||
|
(* 패턴을 검사용 형태로 줄인다. 바인딩은 와일드카드와 같다 —
|
||||||
|
무엇을 덮는가만 중요하다. *)
|
||||||
|
type cpat = CWild | CCtor of string * cpat list
|
||||||
|
type ctor = { c_name : string; c_args : T.t list }
|
||||||
|
type ctors = Finite of ctor list | Infinite
|
||||||
|
|
||||||
|
type env = {
|
||||||
|
(* enum 이름 -> variant 목록. 제네릭은 인스턴스화해서 넘어온다 *)
|
||||||
|
variants : string -> T.t list -> (string * T.t list) list option;
|
||||||
|
is_ctor : string -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ctors_of env (t : T.t) : ctors =
|
||||||
|
match T.resolve t with
|
||||||
|
| T.TBool ->
|
||||||
|
Finite
|
||||||
|
[ { c_name = "true"; c_args = [] }; { c_name = "false"; c_args = [] } ]
|
||||||
|
| T.TUnit -> Finite [ { c_name = "unit"; c_args = [] } ]
|
||||||
|
| T.TCon ("Option", [ a ]) ->
|
||||||
|
Finite
|
||||||
|
[
|
||||||
|
{ c_name = "Some"; c_args = [ a ] }; { c_name = "None"; c_args = [] };
|
||||||
|
]
|
||||||
|
| T.TCon ("Result", [ a; b ]) ->
|
||||||
|
Finite
|
||||||
|
[
|
||||||
|
{ c_name = "Ok"; c_args = [ a ] }; { c_name = "Err"; c_args = [ b ] };
|
||||||
|
]
|
||||||
|
| T.TCon (n, args) -> (
|
||||||
|
match env.variants n args with
|
||||||
|
| Some vs ->
|
||||||
|
Finite
|
||||||
|
(List.map (fun (name, tys) -> { c_name = name; c_args = tys }) vs)
|
||||||
|
| None -> Infinite)
|
||||||
|
| _ -> Infinite
|
||||||
|
|
||||||
|
let rec of_pattern env (p : Ast.pattern) : cpat =
|
||||||
|
match p with
|
||||||
|
| Ast.P_wild _ -> CWild
|
||||||
|
| Ast.P_lit (Ast.L_bool true, _) -> CCtor ("true", [])
|
||||||
|
| Ast.P_lit (Ast.L_bool false, _) -> CCtor ("false", [])
|
||||||
|
(* 리터럴은 인자 없는 생성자다. 와일드카드로 바꾸면 모든 값을 덮는 것이 되어
|
||||||
|
Int 리터럴 몇 개로 exhaustive가 되어버린다. 타입의 생성자 집합이 무한하므로
|
||||||
|
리터럴만으로는 결코 완전해지지 않는다. *)
|
||||||
|
| Ast.P_lit (Ast.L_int n, _) -> CCtor ("<" ^ n ^ ">", [])
|
||||||
|
| Ast.P_lit (Ast.L_str v, _) -> CCtor ("<" ^ String.escaped v ^ ">", [])
|
||||||
|
| Ast.P_bind (n, _) -> if env.is_ctor n then CCtor (n, []) else CWild
|
||||||
|
| Ast.P_ctor { modl; name; args; _ } ->
|
||||||
|
let name = match modl with Some a -> a ^ "." ^ name | None -> name in
|
||||||
|
if env.is_ctor name then CCtor (name, List.map (of_pattern env) args)
|
||||||
|
else CWild
|
||||||
|
|
||||||
|
let wilds n = List.init n (fun _ -> CWild)
|
||||||
|
|
||||||
|
(* 행렬을 생성자 c로 특수화한다 *)
|
||||||
|
let specialize (c : ctor) (matrix : cpat list list) : cpat list list =
|
||||||
|
let arity = List.length c.c_args in
|
||||||
|
List.filter_map
|
||||||
|
(fun row ->
|
||||||
|
match row with
|
||||||
|
| CCtor (n, args) :: rest ->
|
||||||
|
if n = c.c_name then Some (args @ rest) else None
|
||||||
|
| CWild :: rest -> Some (wilds arity @ rest)
|
||||||
|
| [] -> None)
|
||||||
|
matrix
|
||||||
|
|
||||||
|
let default_matrix (matrix : cpat list list) : cpat list list =
|
||||||
|
List.filter_map
|
||||||
|
(fun row ->
|
||||||
|
match row with
|
||||||
|
| CCtor _ :: _ -> None
|
||||||
|
| CWild :: rest -> Some rest
|
||||||
|
| [] -> None)
|
||||||
|
matrix
|
||||||
|
|
||||||
|
let head_names (matrix : cpat list list) =
|
||||||
|
List.filter_map
|
||||||
|
(fun row -> match row with CCtor (n, _) :: _ -> Some n | _ -> None)
|
||||||
|
matrix
|
||||||
|
|
||||||
|
(* 행렬이 덮지 못하는 반례 벡터를 찾는다. None이면 완전하다. *)
|
||||||
|
let rec witness env (matrix : cpat list list) (tys : T.t list) :
|
||||||
|
cpat list option =
|
||||||
|
match tys with
|
||||||
|
| [] -> if matrix = [] then Some [] else None
|
||||||
|
| th :: rest -> (
|
||||||
|
let heads = head_names matrix in
|
||||||
|
match ctors_of env th with
|
||||||
|
| Finite cs
|
||||||
|
when List.for_all (fun c -> List.mem c.c_name heads) cs && cs <> [] ->
|
||||||
|
(* 모든 생성자가 나타났다: 각각으로 파고든다 *)
|
||||||
|
let rec try_each = function
|
||||||
|
| [] -> None
|
||||||
|
| c :: more -> (
|
||||||
|
let arity = List.length c.c_args in
|
||||||
|
match witness env (specialize c matrix) (c.c_args @ rest) with
|
||||||
|
| Some ws ->
|
||||||
|
let args = List.filteri (fun i _ -> i < arity) ws in
|
||||||
|
let tail = List.filteri (fun i _ -> i >= arity) ws in
|
||||||
|
Some (CCtor (c.c_name, args) :: tail)
|
||||||
|
| None -> try_each more)
|
||||||
|
in
|
||||||
|
try_each cs
|
||||||
|
| kind -> (
|
||||||
|
(* 빠진 생성자가 있거나 집합이 무한하다 *)
|
||||||
|
match witness env (default_matrix matrix) rest with
|
||||||
|
| None -> None
|
||||||
|
| Some ws ->
|
||||||
|
let head =
|
||||||
|
match kind with
|
||||||
|
| Finite cs -> (
|
||||||
|
match
|
||||||
|
List.find_opt (fun c -> not (List.mem c.c_name heads)) cs
|
||||||
|
with
|
||||||
|
| Some c -> CCtor (c.c_name, wilds (List.length c.c_args))
|
||||||
|
| None -> CWild)
|
||||||
|
| Infinite -> CWild
|
||||||
|
in
|
||||||
|
Some (head :: ws)))
|
||||||
|
|
||||||
|
let rec show_cpat = function
|
||||||
|
| CWild -> "_"
|
||||||
|
| CCtor (n, []) -> n
|
||||||
|
| CCtor (n, args) ->
|
||||||
|
n ^ "(" ^ String.concat ", " (List.map show_cpat args) ^ ")"
|
||||||
|
|
||||||
|
(* 행 q가 행렬 P에 대해 쓸모 있는가 = P가 덮지 못하는 값을 q가 덮는가 *)
|
||||||
|
let useful env (matrix : cpat list list) (q : cpat list) (tys : T.t list) : bool
|
||||||
|
=
|
||||||
|
let rec go matrix q tys =
|
||||||
|
match (q, tys) with
|
||||||
|
| [], [] -> matrix = []
|
||||||
|
| qh :: qt, th :: tt -> (
|
||||||
|
match qh with
|
||||||
|
| CCtor (n, args) -> (
|
||||||
|
match ctors_of env th with
|
||||||
|
| Finite cs -> (
|
||||||
|
match List.find_opt (fun c -> c.c_name = n) cs with
|
||||||
|
| Some c -> go (specialize c matrix) (args @ qt) (c.c_args @ tt)
|
||||||
|
| None -> go (default_matrix matrix) qt tt)
|
||||||
|
| Infinite ->
|
||||||
|
let c =
|
||||||
|
{ c_name = n; c_args = List.map (fun _ -> T.TUnknown) args }
|
||||||
|
in
|
||||||
|
go (specialize c matrix) (args @ qt) (c.c_args @ tt))
|
||||||
|
| CWild -> (
|
||||||
|
let heads = head_names matrix in
|
||||||
|
match ctors_of env th with
|
||||||
|
| Finite cs
|
||||||
|
when List.for_all (fun c -> List.mem c.c_name heads) cs
|
||||||
|
&& cs <> [] ->
|
||||||
|
List.exists
|
||||||
|
(fun c ->
|
||||||
|
go (specialize c matrix)
|
||||||
|
(wilds (List.length c.c_args) @ qt)
|
||||||
|
(c.c_args @ tt))
|
||||||
|
cs
|
||||||
|
| _ -> go (default_matrix matrix) qt tt))
|
||||||
|
| _ -> false
|
||||||
|
in
|
||||||
|
go matrix q tys
|
||||||
|
|
||||||
|
type result = {
|
||||||
|
missing : string option; (* 빠진 경우의 반례 *)
|
||||||
|
unreachable : int list; (* 도달할 수 없는 팔의 번호 (0부터) *)
|
||||||
|
}
|
||||||
|
|
||||||
|
let check env (scrutinee : T.t) (pats : Ast.pattern list) : result =
|
||||||
|
match ctors_of env scrutinee with
|
||||||
|
| Infinite when T.resolve scrutinee = T.TUnknown ->
|
||||||
|
(* 생성자 집합을 모르면 검사하지 않는다 *)
|
||||||
|
{ missing = None; unreachable = [] }
|
||||||
|
| _ ->
|
||||||
|
let rows = List.map (fun p -> [ of_pattern env p ]) pats in
|
||||||
|
let unreachable =
|
||||||
|
let acc = ref [] in
|
||||||
|
List.iteri
|
||||||
|
(fun i row ->
|
||||||
|
let before = List.filteri (fun j _ -> j < i) rows in
|
||||||
|
if not (useful env before row [ scrutinee ]) then acc := i :: !acc)
|
||||||
|
rows;
|
||||||
|
List.rev !acc
|
||||||
|
in
|
||||||
|
let missing =
|
||||||
|
(* 리터럴 패턴이 섞이면 정확한 반례를 만들 수 없다 — 그 열은 무한
|
||||||
|
집합이므로 와일드카드가 없으면 불완전으로 본다 *)
|
||||||
|
match witness env rows [ scrutinee ] with
|
||||||
|
| Some ws -> (
|
||||||
|
match ws with [ w ] -> Some (show_cpat w) | _ -> Some "_")
|
||||||
|
| None -> None
|
||||||
|
in
|
||||||
|
{ missing; unreachable }
|
||||||
+201
@@ -0,0 +1,201 @@
|
|||||||
|
(* interface artifact와 그 해시.
|
||||||
|
|
||||||
|
hash 입력 = 모듈 exported surface 전체의 의미적 정규형이다 (문서 P7).
|
||||||
|
함수 시그니처(effect 포함), 타입 정의 본문(struct 필드, enum variant),
|
||||||
|
타입의 affinity, 상수의 타입과 값, capability 선언, 그리고 reexport된
|
||||||
|
선언을 완전히 해소한 정의 본문.
|
||||||
|
|
||||||
|
원칙: downstream 검사 결과에 영향을 줄 수 있는 모든 것을 포함한다.
|
||||||
|
의심스러우면 넣는다 — 과잉 포함의 비용은 재검사지만 누락의 비용은
|
||||||
|
잘못된 캐시라는 비대칭이 있다.
|
||||||
|
|
||||||
|
함수 본문은 들어가지 않는다. 본문 한 줄 수정이 해시를 흔들면 incremental
|
||||||
|
전제가 무너진다. *)
|
||||||
|
|
||||||
|
open Ast
|
||||||
|
|
||||||
|
type t = { items : item list; (* 본문을 벗긴 exported surface *) hash : string }
|
||||||
|
|
||||||
|
let strip_body (d : fn_decl) = { d with fn_body = None }
|
||||||
|
|
||||||
|
let is_exported = function
|
||||||
|
| I_fn { pub; _ } -> pub
|
||||||
|
| I_struct { pub; _ } -> pub
|
||||||
|
| I_enum { pub; _ } -> pub
|
||||||
|
| I_capability { pub; _ } -> pub
|
||||||
|
| I_const { pub; _ } -> pub
|
||||||
|
| I_reexport _ -> true
|
||||||
|
(* 테스트는 표면이 아니다. 테스트를 고쳤다고 downstream이 재검사되면
|
||||||
|
안 된다 — 함수 본문과 같은 이유다 *)
|
||||||
|
| I_test _ -> false
|
||||||
|
| I_import _ -> false
|
||||||
|
|
||||||
|
let strip = function
|
||||||
|
| I_fn { pub; decl } -> I_fn { pub; decl = strip_body decl }
|
||||||
|
| it -> it
|
||||||
|
|
||||||
|
let item_name = function
|
||||||
|
| I_fn { decl; _ } -> decl.fn_name
|
||||||
|
| I_struct { name; _ } -> name
|
||||||
|
| I_enum { name; _ } -> name
|
||||||
|
| I_capability { name; _ } -> name
|
||||||
|
| I_const { name; _ } -> name
|
||||||
|
| I_reexport { name; _ } -> name
|
||||||
|
| I_test { name; _ } -> name
|
||||||
|
| I_import { alias; _ } -> alias
|
||||||
|
|
||||||
|
(* reexport는 이름이 아니라 해소된 정의 본문이 hash에 들어간다.
|
||||||
|
A의 enum에 variant가 추가되면 B의 소스가 그대로여도 B의 hash가 변하고,
|
||||||
|
C의 exhaustive match가 재검사된다. *)
|
||||||
|
let surface (m : modul) : item list =
|
||||||
|
let is_definition = function
|
||||||
|
| I_reexport _ | I_import _ -> false
|
||||||
|
| _ -> true
|
||||||
|
in
|
||||||
|
let find name =
|
||||||
|
List.find_opt (fun it -> is_definition it && item_name it = name) m.items
|
||||||
|
in
|
||||||
|
List.concat_map
|
||||||
|
(fun it ->
|
||||||
|
match it with
|
||||||
|
| I_reexport { name; _ } -> (
|
||||||
|
match find name with Some d -> [ strip d ] | None -> [])
|
||||||
|
| it when is_exported it && is_definition it -> [ strip it ]
|
||||||
|
| _ -> [])
|
||||||
|
m.items
|
||||||
|
|
||||||
|
(* 정규형: 항목을 이름순으로 정렬해 선언 순서가 해시에 새지 않게 한다.
|
||||||
|
소스에서 함수 둘의 위치를 바꾸는 것은 downstream에 아무 영향이 없다. *)
|
||||||
|
let render (items : item list) : string =
|
||||||
|
items |> List.map show_item |> List.sort compare |> String.concat "\n"
|
||||||
|
|
||||||
|
let of_module (m : modul) : t =
|
||||||
|
let items = surface m in
|
||||||
|
{ items; hash = Digest.to_hex (Digest.string (render items)) }
|
||||||
|
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
(* 소비 측 한정 *)
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
(* 가져온 모듈의 exported surface를 소비 측 이름 공간으로 옮긴다.
|
||||||
|
`import "shapes" as Shapes`라면 Shape는 "Shapes.Shape"가 된다.
|
||||||
|
|
||||||
|
왜 소비 시점인가 — 별칭은 가져오는 쪽의 선택이므로 정의한 모듈의
|
||||||
|
interface hash에 새어서는 안 된다. of_module은 한정하지 않은 표면을
|
||||||
|
해시하고, 한정은 여기서만 한다.
|
||||||
|
|
||||||
|
v0의 한계 두 가지, 의도적으로 남긴다:
|
||||||
|
- 가져온 모듈이 다시 다른 모듈의 타입을 참조하면(전이 참조) 불투명해진다.
|
||||||
|
- effect atom의 capability 이름은 한정하지 않는다. 즉 effect 이름은 v0에서
|
||||||
|
전역이다. 모듈별 identity는 v1 과제다. *)
|
||||||
|
|
||||||
|
let builtin_ty_names =
|
||||||
|
[ "Int"; "Bool"; "String"; "Unit"; "List"; "Option"; "Result"; "TaskScope" ]
|
||||||
|
|
||||||
|
let opaque pos = T_named { modl = None; name = "«외부»"; args = []; pos }
|
||||||
|
|
||||||
|
let rec q_ty alias defined gen (t : ty) : ty =
|
||||||
|
match t with
|
||||||
|
| T_named { modl = Some _; pos; _ } -> opaque pos
|
||||||
|
| T_named { modl = None; name; args; pos } ->
|
||||||
|
let args = List.map (q_targ alias defined gen) args in
|
||||||
|
if List.mem name gen || List.mem name builtin_ty_names then
|
||||||
|
T_named { modl = None; name; args; pos }
|
||||||
|
else if List.mem name defined then
|
||||||
|
T_named { modl = None; name = alias ^ "." ^ name; args; pos }
|
||||||
|
else opaque pos
|
||||||
|
| T_fn { affine; params; eff; ret; pos } ->
|
||||||
|
T_fn
|
||||||
|
{
|
||||||
|
affine;
|
||||||
|
params =
|
||||||
|
List.map
|
||||||
|
(fun (p : fn_param_ty) ->
|
||||||
|
{ p with pt_ty = q_ty alias defined gen p.pt_ty })
|
||||||
|
params;
|
||||||
|
eff;
|
||||||
|
ret = Option.map (q_ty alias defined gen) ret;
|
||||||
|
pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
and q_targ alias defined gen = function
|
||||||
|
| TA_ty t -> TA_ty (q_ty alias defined gen t)
|
||||||
|
| TA_eff e -> TA_eff e
|
||||||
|
|
||||||
|
let q_fn alias defined (d : fn_decl) : fn_decl =
|
||||||
|
let gen = List.map (fun g -> g.gp_name) d.fn_gen in
|
||||||
|
{
|
||||||
|
d with
|
||||||
|
fn_params =
|
||||||
|
List.map
|
||||||
|
(fun p -> { p with p_ty = q_ty alias defined gen p.p_ty })
|
||||||
|
d.fn_params;
|
||||||
|
fn_ret = Option.map (q_ty alias defined gen) d.fn_ret;
|
||||||
|
fn_body = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let defined_names (items : item list) =
|
||||||
|
List.filter_map
|
||||||
|
(function
|
||||||
|
| I_struct { name; _ } | I_enum { name; _ } | I_capability { name; _ } ->
|
||||||
|
Some name
|
||||||
|
| _ -> None)
|
||||||
|
items
|
||||||
|
|
||||||
|
let qualify (alias : string) (items : item list) : item list =
|
||||||
|
let defined = defined_names items in
|
||||||
|
let p n = alias ^ "." ^ n in
|
||||||
|
List.map
|
||||||
|
(fun it ->
|
||||||
|
match it with
|
||||||
|
| I_fn { pub; decl } ->
|
||||||
|
I_fn
|
||||||
|
{
|
||||||
|
pub;
|
||||||
|
decl = { (q_fn alias defined decl) with fn_name = p decl.fn_name };
|
||||||
|
}
|
||||||
|
| I_struct { pub; copyable; name; gen; fields; pos } ->
|
||||||
|
let g = List.map (fun x -> x.gp_name) gen in
|
||||||
|
I_struct
|
||||||
|
{
|
||||||
|
pub;
|
||||||
|
copyable;
|
||||||
|
name = p name;
|
||||||
|
gen;
|
||||||
|
fields =
|
||||||
|
List.map
|
||||||
|
(fun f -> { f with f_ty = q_ty alias defined g f.f_ty })
|
||||||
|
fields;
|
||||||
|
pos;
|
||||||
|
}
|
||||||
|
| I_enum { pub; name; gen; variants; pos } ->
|
||||||
|
let g = List.map (fun x -> x.gp_name) gen in
|
||||||
|
I_enum
|
||||||
|
{
|
||||||
|
pub;
|
||||||
|
name = p name;
|
||||||
|
gen;
|
||||||
|
variants =
|
||||||
|
List.map
|
||||||
|
(fun v ->
|
||||||
|
{
|
||||||
|
v with
|
||||||
|
v_name = p v.v_name;
|
||||||
|
v_args = List.map (q_ty alias defined g) v.v_args;
|
||||||
|
})
|
||||||
|
variants;
|
||||||
|
pos;
|
||||||
|
}
|
||||||
|
| I_capability { pub; name; methods; pos } ->
|
||||||
|
I_capability
|
||||||
|
{
|
||||||
|
pub;
|
||||||
|
name = p name;
|
||||||
|
methods = List.map (q_fn alias defined) methods;
|
||||||
|
pos;
|
||||||
|
}
|
||||||
|
| I_const { pub; name; ty; value; pos } ->
|
||||||
|
I_const
|
||||||
|
{ pub; name = p name; ty = q_ty alias defined [] ty; value; pos }
|
||||||
|
| it -> it)
|
||||||
|
items
|
||||||
+556
@@ -0,0 +1,556 @@
|
|||||||
|
(* 트리 워킹 인터프리터.
|
||||||
|
|
||||||
|
여기 도달한 프로그램은 이미 타입, effect, capability, ownership 검사를
|
||||||
|
통과했다. 그러므로 이 파일은 검사하지 않는다 — 검사기가 이미 답한 질문을
|
||||||
|
실행 시점에 다시 묻는 것은 두 번째 진실을 만드는 일이다.
|
||||||
|
|
||||||
|
실행 시점 오류로 남는 것은 검사기가 원리적으로 못 잡는 것뿐이다:
|
||||||
|
0으로 나누기, 리스트 범위, 그리고 아직 없는 표준 라이브러리 이름.
|
||||||
|
|
||||||
|
권한의 유일한 출처는 런타임이다. 소스에는 capability를 만드는 문법이 없고,
|
||||||
|
main은 자기가 선언한 capability만 받는다. 선언하지 않은 권한은 프로그램
|
||||||
|
어디에도 존재하지 않는다 — 보안 정리 (i)의 실행 시점 대응물이다. *)
|
||||||
|
|
||||||
|
type value =
|
||||||
|
| VUnit
|
||||||
|
| VInt of int
|
||||||
|
| VBool of bool
|
||||||
|
| VStr of string
|
||||||
|
| VList of value list
|
||||||
|
| VStruct of string * (string * value ref) list
|
||||||
|
| VEnum of string * string * value list (* enum, variant, 인자 *)
|
||||||
|
| VClosure of { params : string list; body : Ir.t; env : env }
|
||||||
|
| VFn of Ir.fn
|
||||||
|
| VCtor of string * string * int (* 아직 인자를 안 받은 생성자 *)
|
||||||
|
| VBuiltin of string
|
||||||
|
| VNative of (value list -> value)
|
||||||
|
| VCap of string * (string * (value list -> value)) list
|
||||||
|
| VScope of string
|
||||||
|
|
||||||
|
and env = (string * value ref) list list
|
||||||
|
|
||||||
|
exception Return_exc of value
|
||||||
|
exception Fail of Token.pos * string
|
||||||
|
|
||||||
|
let fail pos msg = raise (Fail (pos, msg))
|
||||||
|
|
||||||
|
let rec show = function
|
||||||
|
| VUnit -> "unit"
|
||||||
|
| VInt n -> string_of_int n
|
||||||
|
| VBool b -> if b then "true" else "false"
|
||||||
|
| VStr s -> s
|
||||||
|
| VList xs -> "[" ^ String.concat ", " (List.map show xs) ^ "]"
|
||||||
|
| VStruct (n, fs) ->
|
||||||
|
n ^ "{"
|
||||||
|
^ String.concat ", " (List.map (fun (k, v) -> k ^ ": " ^ show !v) fs)
|
||||||
|
^ "}"
|
||||||
|
| VEnum (_, v, []) -> v
|
||||||
|
| VEnum (_, v, args) ->
|
||||||
|
v ^ "(" ^ String.concat ", " (List.map show args) ^ ")"
|
||||||
|
| VClosure _ | VFn _ | VBuiltin _ | VCtor _ | VNative _ -> "<fn>"
|
||||||
|
| VCap (n, _) -> "<capability " ^ n ^ ">"
|
||||||
|
| VScope n -> "<scope " ^ n ^ ">"
|
||||||
|
|
||||||
|
let rec eq a b =
|
||||||
|
match (a, b) with
|
||||||
|
| VInt x, VInt y -> x = y
|
||||||
|
| VBool x, VBool y -> x = y
|
||||||
|
| VStr x, VStr y -> x = y
|
||||||
|
| VUnit, VUnit -> true
|
||||||
|
| VList x, VList y -> List.length x = List.length y && List.for_all2 eq x y
|
||||||
|
| VEnum (_, v1, a1), VEnum (_, v2, a2) ->
|
||||||
|
v1 = v2 && List.length a1 = List.length a2 && List.for_all2 eq a1 a2
|
||||||
|
| _ -> false
|
||||||
|
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
(* 환경 *)
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
let lookup (env : env) n =
|
||||||
|
let rec go = function
|
||||||
|
| [] -> None
|
||||||
|
| s :: r -> (
|
||||||
|
match List.assoc_opt n s with Some v -> Some v | None -> go r)
|
||||||
|
in
|
||||||
|
go env
|
||||||
|
|
||||||
|
let bind (env : env) n v : env =
|
||||||
|
match env with s :: r -> ((n, ref v) :: s) :: r | [] -> [ [ (n, ref v) ] ]
|
||||||
|
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
(* 런타임이 제공하는 것 *)
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
(* 문자열 도우미. 언어에 인덱싱 연산자가 없으므로 이 일은 런타임 몫이다. *)
|
||||||
|
let find_sub hay needle =
|
||||||
|
let n = String.length needle and h = String.length hay in
|
||||||
|
let rec go i =
|
||||||
|
if i + n > h then None
|
||||||
|
else if String.sub hay i n = needle then Some i
|
||||||
|
else go (i + 1)
|
||||||
|
in
|
||||||
|
if n = 0 then Some 0 else go 0
|
||||||
|
|
||||||
|
let split_on s sep =
|
||||||
|
if sep = "" then [ s ]
|
||||||
|
else
|
||||||
|
let n = String.length sep in
|
||||||
|
let rec go s acc =
|
||||||
|
match find_sub s sep with
|
||||||
|
| None -> List.rev (s :: acc)
|
||||||
|
| Some i ->
|
||||||
|
go
|
||||||
|
(String.sub s (i + n) (String.length s - i - n))
|
||||||
|
(String.sub s 0 i :: acc)
|
||||||
|
in
|
||||||
|
go s []
|
||||||
|
|
||||||
|
let out = Buffer.create 1024
|
||||||
|
|
||||||
|
(* 프로그램 인자. 런타임이 들고 있다가 Args capability를 통해서만 준다 —
|
||||||
|
전역 변수로 아무 데서나 읽을 수 있으면 그것이 ambient authority다. *)
|
||||||
|
let argv : string list ref = ref []
|
||||||
|
|
||||||
|
let read_whole path =
|
||||||
|
let ic = open_in_bin path in
|
||||||
|
let n = in_channel_length ic in
|
||||||
|
let s = really_input_string ic n in
|
||||||
|
close_in ic;
|
||||||
|
s
|
||||||
|
|
||||||
|
(* IO 오류는 문자열로 돌려준다. 런타임이 사용자 정의 enum을 만들 수는 없고,
|
||||||
|
만들 수 있게 하면 런타임이 프로그램의 타입을 알아야 한다. v0의 선은
|
||||||
|
여기다 — Result[a, String]. *)
|
||||||
|
let root_capability name : value option =
|
||||||
|
match name with
|
||||||
|
| "Console" ->
|
||||||
|
Some
|
||||||
|
(VCap
|
||||||
|
( "Console",
|
||||||
|
[
|
||||||
|
( "print",
|
||||||
|
fun args ->
|
||||||
|
List.iter (fun v -> Buffer.add_string out (show v)) args;
|
||||||
|
Buffer.add_char out '\n';
|
||||||
|
VUnit );
|
||||||
|
] ))
|
||||||
|
| "File" ->
|
||||||
|
Some
|
||||||
|
(VCap
|
||||||
|
( "File",
|
||||||
|
[
|
||||||
|
( "read",
|
||||||
|
fun args ->
|
||||||
|
match args with
|
||||||
|
| [ VStr path ] -> (
|
||||||
|
try VEnum ("Result", "Ok", [ VStr (read_whole path) ])
|
||||||
|
with Sys_error m -> VEnum ("Result", "Err", [ VStr m ]))
|
||||||
|
| _ -> VEnum ("Result", "Err", [ VStr "read: 경로가 필요합니다" ]) );
|
||||||
|
] ))
|
||||||
|
| "Args" ->
|
||||||
|
Some
|
||||||
|
(VCap
|
||||||
|
( "Args",
|
||||||
|
[ ("all", fun _ -> VList (List.map (fun s -> VStr s) !argv)) ] ))
|
||||||
|
| "TaskScope" ->
|
||||||
|
(* 루트 스코프. 구조적 동시성의 뿌리도 런타임이 준다 — 프로그램이
|
||||||
|
스스로 만들 수 있으면 부모 없는 작업이 생긴다. *)
|
||||||
|
Some (VScope "root")
|
||||||
|
| _ -> None
|
||||||
|
|
||||||
|
(* 런타임이 구현한 이름 전부. std/*.cool의 선언과 이 목록이 어긋나면 검사는
|
||||||
|
통과하고 실행이 죽는다 — 그 간극을 테스트가 막는다 (docs/friction.md F7).
|
||||||
|
v1에서 std를 coollang으로 구현하면 이 목록 자체가 사라진다. *)
|
||||||
|
let implemented =
|
||||||
|
[
|
||||||
|
"string.len";
|
||||||
|
"string.is_empty";
|
||||||
|
"string.concat";
|
||||||
|
"string.split";
|
||||||
|
"string.join";
|
||||||
|
"string.trim";
|
||||||
|
"string.starts_with";
|
||||||
|
"string.contains";
|
||||||
|
"int.show";
|
||||||
|
"int.abs";
|
||||||
|
"int.parse";
|
||||||
|
"bool.show";
|
||||||
|
"list.len";
|
||||||
|
"list.is_empty";
|
||||||
|
"list.first";
|
||||||
|
"list.nth";
|
||||||
|
"list.rest";
|
||||||
|
"list.push";
|
||||||
|
"list.concat";
|
||||||
|
"list.reverse";
|
||||||
|
"list.enumerate";
|
||||||
|
"list.each";
|
||||||
|
"list.map";
|
||||||
|
"list.filter";
|
||||||
|
"list.fold";
|
||||||
|
"option.is_some";
|
||||||
|
"option.map";
|
||||||
|
"option.unwrap_or";
|
||||||
|
"option.ok_or";
|
||||||
|
"result.is_ok";
|
||||||
|
"result.map";
|
||||||
|
"result.map_err";
|
||||||
|
"result.unwrap_or";
|
||||||
|
]
|
||||||
|
|
||||||
|
let builtin pos name (args : value list) : value =
|
||||||
|
match (name, args) with
|
||||||
|
| "string.concat", [ VStr a; VStr b ] -> VStr (a ^ b)
|
||||||
|
| "string.len", [ VStr a ] -> VInt (String.length a)
|
||||||
|
| "string.is_empty", [ VStr a ] -> VBool (a = "")
|
||||||
|
| "string.split", [ VStr s; VStr sep ] ->
|
||||||
|
VList (List.map (fun x -> VStr x) (split_on s sep))
|
||||||
|
| "string.trim", [ VStr s ] -> VStr (String.trim s)
|
||||||
|
| "string.starts_with", [ VStr s; VStr p ] ->
|
||||||
|
VBool
|
||||||
|
(String.length s >= String.length p
|
||||||
|
&& String.sub s 0 (String.length p) = p)
|
||||||
|
| "string.contains", [ VStr s; VStr n ] -> VBool (find_sub s n <> None)
|
||||||
|
| "int.show", [ VInt n ] -> VStr (string_of_int n)
|
||||||
|
| "int.abs", [ VInt n ] -> VInt (abs n)
|
||||||
|
| "int.parse", [ VStr s ] -> (
|
||||||
|
match int_of_string_opt (String.trim s) with
|
||||||
|
| Some n -> VEnum ("Result", "Ok", [ VInt n ])
|
||||||
|
| None -> VEnum ("Result", "Err", [ VStr (s ^ "은(는) 정수가 아닙니다") ]))
|
||||||
|
| "bool.show", [ VBool b ] -> VStr (if b then "true" else "false")
|
||||||
|
| "list.len", [ VList xs ] -> VInt (List.length xs)
|
||||||
|
| "list.is_empty", [ VList xs ] -> VBool (xs = [])
|
||||||
|
| "list.push", [ VList xs; x ] -> VList (xs @ [ x ])
|
||||||
|
| "list.concat", [ VList xs; VList ys ] -> VList (xs @ ys)
|
||||||
|
| "list.reverse", [ VList xs ] -> VList (List.rev xs)
|
||||||
|
| "list.rest", [ VList xs ] -> (
|
||||||
|
match xs with [] -> VList [] | _ :: r -> VList r)
|
||||||
|
| "list.first", [ VList xs ] -> (
|
||||||
|
match xs with
|
||||||
|
| [] -> VEnum ("Option", "None", [])
|
||||||
|
| x :: _ -> VEnum ("Option", "Some", [ x ]))
|
||||||
|
| "list.enumerate", [ VList xs ] ->
|
||||||
|
VList
|
||||||
|
(List.mapi
|
||||||
|
(fun i x ->
|
||||||
|
VStruct ("List.Indexed", [ ("i", ref (VInt i)); ("value", ref x) ]))
|
||||||
|
xs)
|
||||||
|
| "list.nth", [ VList xs; VInt i ] -> (
|
||||||
|
match List.nth_opt xs i with
|
||||||
|
| Some x -> VEnum ("Option", "Some", [ x ])
|
||||||
|
| None -> VEnum ("Option", "None", []))
|
||||||
|
| "string.join", [ VStr sep; VList parts ] ->
|
||||||
|
VStr
|
||||||
|
(String.concat sep
|
||||||
|
(List.map (function VStr s -> s | v -> show v) parts))
|
||||||
|
| "option.is_some", [ VEnum ("Option", v, _) ] -> VBool (v = "Some")
|
||||||
|
| "option.unwrap_or", [ VEnum ("Option", "Some", [ x ]); _ ] -> x
|
||||||
|
| "option.unwrap_or", [ _; fallback ] -> fallback
|
||||||
|
| "option.ok_or", [ VEnum ("Option", "Some", [ x ]); _ ] ->
|
||||||
|
VEnum ("Result", "Ok", [ x ])
|
||||||
|
| "option.ok_or", [ _; e ] -> VEnum ("Result", "Err", [ e ])
|
||||||
|
| "result.is_ok", [ VEnum ("Result", v, _) ] -> VBool (v = "Ok")
|
||||||
|
| "result.unwrap_or", [ VEnum ("Result", "Ok", [ x ]); _ ] -> x
|
||||||
|
| "result.unwrap_or", [ _; fallback ] -> fallback
|
||||||
|
| _ ->
|
||||||
|
fail pos (Printf.sprintf "%s은(는) 런타임이 제공하지 않습니다 (표준 라이브러리가 아직 없습니다)" name)
|
||||||
|
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
(* 실행 *)
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
type st = { prog : Ir.program }
|
||||||
|
|
||||||
|
let rec eval st (env : env) (e : Ir.t) : value =
|
||||||
|
match e with
|
||||||
|
| Ir.I_unit -> VUnit
|
||||||
|
| Ir.I_lit (Ast.L_int n) -> VInt (int_of_string n)
|
||||||
|
| Ir.I_lit (Ast.L_str s) -> VStr s
|
||||||
|
| Ir.I_lit (Ast.L_bool b) -> VBool b
|
||||||
|
| Ir.I_ref (k, pos) -> eval_ref st env k pos
|
||||||
|
| Ir.I_list xs -> VList (List.map (eval st env) xs)
|
||||||
|
| Ir.I_make (n, fields) ->
|
||||||
|
VStruct (n, List.map (fun (k, e) -> (k, ref (eval st env e))) fields)
|
||||||
|
| Ir.I_closure c -> VClosure { params = c.c_params; body = c.c_body; env }
|
||||||
|
| Ir.I_if { cond; then_; else_ } -> (
|
||||||
|
match eval st env cond with
|
||||||
|
| VBool true -> eval st ([] :: env) then_
|
||||||
|
| _ -> eval st ([] :: env) else_)
|
||||||
|
| Ir.I_match { scrutinee; arms; pos } ->
|
||||||
|
let v = eval st env scrutinee in
|
||||||
|
let rec go = function
|
||||||
|
| [] -> fail pos "match에서 어떤 팔도 맞지 않았습니다"
|
||||||
|
| (p, body) :: rest -> (
|
||||||
|
match match_pat v p with
|
||||||
|
| None -> go rest
|
||||||
|
| Some binds ->
|
||||||
|
let env =
|
||||||
|
List.fold_left (fun e (n, v) -> bind e n v) ([] :: env) binds
|
||||||
|
in
|
||||||
|
eval st env body)
|
||||||
|
in
|
||||||
|
go arms
|
||||||
|
| Ir.I_scope { name; body; _ } ->
|
||||||
|
(* v0의 실행 의미: 자식 작업은 순차로 돈다. 블록을 나가는 것이 join이다.
|
||||||
|
구조가 먼저고 병렬성은 그 위의 최적화다 — 순서가 반대면 취소와
|
||||||
|
전파를 나중에 끼워 넣게 된다. *)
|
||||||
|
let env = bind ([] :: env) name (VScope name) in
|
||||||
|
eval st env body
|
||||||
|
| Ir.I_seq (stmts, tail) ->
|
||||||
|
let env = List.fold_left (fun env s -> exec st env s) ([] :: env) stmts in
|
||||||
|
eval st env tail
|
||||||
|
| Ir.I_call { callee; args; pos } ->
|
||||||
|
let f = eval st env callee in
|
||||||
|
let args = List.map (eval st env) args in
|
||||||
|
apply st pos f args
|
||||||
|
| Ir.I_field { obj; name; pos } -> (
|
||||||
|
match eval st env obj with
|
||||||
|
| VStruct (sn, fields) -> (
|
||||||
|
match List.assoc_opt name fields with
|
||||||
|
| Some r -> !r
|
||||||
|
| None -> fail pos (Printf.sprintf "%s에 %s 필드가 없습니다" sn name))
|
||||||
|
| VCap (cn, meths) -> (
|
||||||
|
(* capability 메서드는 값을 통해서만 나온다. 여기가 권한이 코드로
|
||||||
|
흐르는 유일한 통로다. *)
|
||||||
|
match List.assoc_opt name meths with
|
||||||
|
| Some f -> VNative f
|
||||||
|
| None ->
|
||||||
|
fail pos (Printf.sprintf "capability %s에 %s이(가) 없습니다" cn name))
|
||||||
|
| VScope _ when name = "spawn" ->
|
||||||
|
VNative
|
||||||
|
(fun args ->
|
||||||
|
match args with
|
||||||
|
| [ f ] -> apply st pos f []
|
||||||
|
| _ -> fail pos "spawn은 함수 하나를 받습니다")
|
||||||
|
| other -> fail pos (Printf.sprintf "%s에는 필드가 없습니다" (show other)))
|
||||||
|
| Ir.I_crash (msg, pos) -> (
|
||||||
|
match eval st env msg with VStr s -> fail pos s | v -> fail pos (show v))
|
||||||
|
| Ir.I_unary (op, e, pos) -> (
|
||||||
|
match (op, eval st env e) with
|
||||||
|
| Ast.U_not, VBool b -> VBool (not b)
|
||||||
|
| Ast.U_neg, VInt n -> VInt (-n)
|
||||||
|
| _ -> fail pos "단항 연산자의 피연산자가 맞지 않습니다")
|
||||||
|
| Ir.I_binary (op, a, b, pos) -> eval_binary st env op a b pos
|
||||||
|
|
||||||
|
and eval_ref st env k pos =
|
||||||
|
match k with
|
||||||
|
| Ir.R_local n -> (
|
||||||
|
match lookup env n with
|
||||||
|
| Some r -> !r
|
||||||
|
| None -> fail pos (Printf.sprintf "%s이(가) 묶여 있지 않습니다" n))
|
||||||
|
| Ir.R_ctor (enum, name) -> (
|
||||||
|
match Hashtbl.find_opt st.prog.Ir.ctors name with
|
||||||
|
| Some (_, 0) -> VEnum (enum, name, [])
|
||||||
|
| Some (_, n) -> VCtor (enum, name, n)
|
||||||
|
| None -> VEnum (enum, name, []))
|
||||||
|
| Ir.R_global n -> (
|
||||||
|
match Hashtbl.find_opt st.prog.Ir.fns n with
|
||||||
|
| Some f -> VFn f
|
||||||
|
| None -> (
|
||||||
|
match Hashtbl.find_opt st.prog.Ir.consts n with
|
||||||
|
| Some e -> eval st [ [] ] e
|
||||||
|
| None -> fail pos (Printf.sprintf "%s을(를) 찾을 수 없습니다" n)))
|
||||||
|
| Ir.R_builtin n -> VBuiltin n
|
||||||
|
|
||||||
|
and apply st pos f args =
|
||||||
|
match f with
|
||||||
|
| VFn fn -> (
|
||||||
|
let env = [ List.map2 (fun p a -> (p, ref a)) fn.Ir.fn_params args ] in
|
||||||
|
try eval st env fn.Ir.fn_body with Return_exc v -> v)
|
||||||
|
| VClosure { params; body; env } -> (
|
||||||
|
let env = List.map2 (fun p a -> (p, ref a)) params args :: env in
|
||||||
|
try eval st env body with Return_exc v -> v)
|
||||||
|
| VCtor (enum, name, _) -> VEnum (enum, name, args)
|
||||||
|
(* 고차 builtin은 여기서 처리한다 — apply를 다시 부를 수 있어야 하므로 *)
|
||||||
|
| VBuiltin "list.each" -> (
|
||||||
|
match args with
|
||||||
|
| [ VList xs; f ] ->
|
||||||
|
List.iter (fun x -> ignore (apply st pos f [ x ])) xs;
|
||||||
|
VUnit
|
||||||
|
| _ -> fail pos "list.each는 리스트와 함수를 받습니다")
|
||||||
|
| VBuiltin "list.map" -> (
|
||||||
|
match args with
|
||||||
|
| [ VList xs; f ] -> VList (List.map (fun x -> apply st pos f [ x ]) xs)
|
||||||
|
| _ -> fail pos "list.map은 리스트와 함수를 받습니다")
|
||||||
|
| VBuiltin "list.filter" -> (
|
||||||
|
match args with
|
||||||
|
| [ VList xs; f ] ->
|
||||||
|
VList
|
||||||
|
(List.filter
|
||||||
|
(fun x ->
|
||||||
|
match apply st pos f [ x ] with VBool b -> b | _ -> false)
|
||||||
|
xs)
|
||||||
|
| _ -> fail pos "list.filter는 리스트와 함수를 받습니다")
|
||||||
|
| VBuiltin "option.map" -> (
|
||||||
|
match args with
|
||||||
|
| [ VEnum ("Option", "Some", [ x ]); f ] ->
|
||||||
|
VEnum ("Option", "Some", [ apply st pos f [ x ] ])
|
||||||
|
| [ o; _ ] -> o
|
||||||
|
| _ -> fail pos "option.map은 Option과 함수를 받습니다")
|
||||||
|
| VBuiltin "result.map" -> (
|
||||||
|
match args with
|
||||||
|
| [ VEnum ("Result", "Ok", [ x ]); f ] ->
|
||||||
|
VEnum ("Result", "Ok", [ apply st pos f [ x ] ])
|
||||||
|
| [ r; _ ] -> r
|
||||||
|
| _ -> fail pos "result.map은 Result와 함수를 받습니다")
|
||||||
|
| VBuiltin "result.map_err" -> (
|
||||||
|
match args with
|
||||||
|
| [ VEnum ("Result", "Err", [ e ]); f ] ->
|
||||||
|
VEnum ("Result", "Err", [ apply st pos f [ e ] ])
|
||||||
|
| [ r; _ ] -> r
|
||||||
|
| _ -> fail pos "result.map_err는 Result와 함수를 받습니다")
|
||||||
|
| VBuiltin "list.fold" -> (
|
||||||
|
match args with
|
||||||
|
| [ VList xs; init; f ] ->
|
||||||
|
List.fold_left (fun acc x -> apply st pos f [ acc; x ]) init xs
|
||||||
|
| _ -> fail pos "list.fold는 리스트, 초기값, 함수를 받습니다")
|
||||||
|
| VBuiltin n -> builtin pos n args
|
||||||
|
| VNative f -> f args
|
||||||
|
| other -> fail pos (Printf.sprintf "%s은(는) 부를 수 없습니다" (show other))
|
||||||
|
|
||||||
|
and exec st env (s : Ir.stmt) : env =
|
||||||
|
match s with
|
||||||
|
| Ir.S_do e ->
|
||||||
|
ignore (eval st env e);
|
||||||
|
env
|
||||||
|
| Ir.S_return (e, _) -> raise (Return_exc (eval st env e))
|
||||||
|
| Ir.S_let (p, e, pos) -> (
|
||||||
|
let v = eval st env e in
|
||||||
|
match match_pat v p with
|
||||||
|
| None -> fail pos "let 패턴이 값과 맞지 않습니다"
|
||||||
|
| Some binds -> List.fold_left (fun e (n, v) -> bind e n v) env binds)
|
||||||
|
| Ir.S_assign { place; value; pos } -> (
|
||||||
|
let v = eval st env value in
|
||||||
|
match place with
|
||||||
|
| Ir.I_ref (Ir.R_local n, _) -> (
|
||||||
|
match lookup env n with
|
||||||
|
| Some r ->
|
||||||
|
r := v;
|
||||||
|
env
|
||||||
|
| None -> fail pos (Printf.sprintf "%s이(가) 묶여 있지 않습니다" n))
|
||||||
|
| Ir.I_field { obj; name; _ } -> (
|
||||||
|
match eval st env obj with
|
||||||
|
| VStruct (_, fields) -> (
|
||||||
|
match List.assoc_opt name fields with
|
||||||
|
| Some r ->
|
||||||
|
r := v;
|
||||||
|
env
|
||||||
|
| None -> fail pos (Printf.sprintf "%s 필드가 없습니다" name))
|
||||||
|
| _ -> fail pos "필드에 대입할 수 없습니다")
|
||||||
|
| _ -> fail pos "대입할 수 없는 자리입니다")
|
||||||
|
|
||||||
|
and match_pat v (p : Ir.pat) : (string * value) list option =
|
||||||
|
match (p, v) with
|
||||||
|
| Ir.IP_wild, _ -> Some []
|
||||||
|
| Ir.IP_bind n, _ -> Some [ (n, v) ]
|
||||||
|
| Ir.IP_lit (Ast.L_int n), VInt m ->
|
||||||
|
if int_of_string n = m then Some [] else None
|
||||||
|
| Ir.IP_lit (Ast.L_str s), VStr t -> if s = t then Some [] else None
|
||||||
|
| Ir.IP_lit (Ast.L_bool b), VBool c -> if b = c then Some [] else None
|
||||||
|
| Ir.IP_ctor (name, ps), VEnum (_, vn, args) ->
|
||||||
|
if name <> vn || List.length ps <> List.length args then None
|
||||||
|
else
|
||||||
|
List.fold_left2
|
||||||
|
(fun acc p a ->
|
||||||
|
match (acc, match_pat a p) with
|
||||||
|
| Some xs, Some ys -> Some (xs @ ys)
|
||||||
|
| _ -> None)
|
||||||
|
(Some []) ps args
|
||||||
|
| _ -> None
|
||||||
|
|
||||||
|
and eval_binary st env op a b pos =
|
||||||
|
match op with
|
||||||
|
(* 단축 평가. 오른쪽을 먼저 계산하면 && 의 의미가 달라진다 *)
|
||||||
|
| Ast.B_and -> (
|
||||||
|
match eval st env a with VBool false -> VBool false | _ -> eval st env b)
|
||||||
|
| Ast.B_or -> (
|
||||||
|
match eval st env a with VBool true -> VBool true | _ -> eval st env b)
|
||||||
|
| _ -> (
|
||||||
|
let x = eval st env a and y = eval st env b in
|
||||||
|
match (op, x, y) with
|
||||||
|
| Ast.B_eq, _, _ -> VBool (eq x y)
|
||||||
|
| Ast.B_ne, _, _ -> VBool (not (eq x y))
|
||||||
|
| Ast.B_lt, VInt m, VInt n -> VBool (m < n)
|
||||||
|
| Ast.B_le, VInt m, VInt n -> VBool (m <= n)
|
||||||
|
| Ast.B_gt, VInt m, VInt n -> VBool (m > n)
|
||||||
|
| Ast.B_ge, VInt m, VInt n -> VBool (m >= n)
|
||||||
|
| Ast.B_add, VInt m, VInt n -> VInt (m + n)
|
||||||
|
| Ast.B_sub, VInt m, VInt n -> VInt (m - n)
|
||||||
|
| Ast.B_mul, VInt m, VInt n -> VInt (m * n)
|
||||||
|
| Ast.B_div, VInt _, VInt 0 -> fail pos "0으로 나눌 수 없습니다"
|
||||||
|
| Ast.B_div, VInt m, VInt n -> VInt (m / n)
|
||||||
|
| Ast.B_rem, VInt _, VInt 0 -> fail pos "0으로 나눌 수 없습니다"
|
||||||
|
| Ast.B_rem, VInt m, VInt n -> VInt (m mod n)
|
||||||
|
| _ -> fail pos "연산자의 피연산자 타입이 맞지 않습니다")
|
||||||
|
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
(* 진입 *)
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
(* main이 선언한 capability만 런타임이 넘긴다. 선언하지 않은 권한은
|
||||||
|
프로그램 안에 존재하지 않는다. *)
|
||||||
|
(* 실패해도 그때까지 나온 출력을 함께 돌려준다.
|
||||||
|
print는 실제로 일어난 effect다. 일어난 일을 안 보여주면 "어디까지 갔나"를
|
||||||
|
알 수 없고, 그게 실패했을 때 가장 먼저 보고 싶은 것이다. *)
|
||||||
|
let run ?(args = []) (prog : Ir.program) (entry : string)
|
||||||
|
(main_params : (string * string) list) :
|
||||||
|
string * (Token.pos * string) option =
|
||||||
|
Buffer.clear out;
|
||||||
|
argv := args;
|
||||||
|
let st = { prog } in
|
||||||
|
match Hashtbl.find_opt prog.Ir.fns (entry ^ "#main") with
|
||||||
|
| None -> ("", Some (Token.{ line = 0; col = 0 }, "main 함수가 없습니다"))
|
||||||
|
| Some fn -> (
|
||||||
|
let args =
|
||||||
|
List.map
|
||||||
|
(fun (_, tyname) ->
|
||||||
|
match root_capability tyname with
|
||||||
|
| Some v -> Ok v
|
||||||
|
| None ->
|
||||||
|
Error (Printf.sprintf "런타임이 %s capability를 제공하지 않습니다" tyname))
|
||||||
|
main_params
|
||||||
|
in
|
||||||
|
match List.find_opt Result.is_error args with
|
||||||
|
| Some (Error m) -> ("", Some (Token.{ line = 0; col = 0 }, m))
|
||||||
|
| _ -> (
|
||||||
|
let args = List.map Result.get_ok args in
|
||||||
|
try
|
||||||
|
ignore (apply st Token.{ line = 0; col = 0 } (VFn fn) args);
|
||||||
|
(Buffer.contents out, None)
|
||||||
|
with
|
||||||
|
| Fail (pos, msg) -> (Buffer.contents out, Some (pos, msg))
|
||||||
|
| Return_exc _ -> (Buffer.contents out, None)))
|
||||||
|
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
(* 테스트 러너 *)
|
||||||
|
(* *)
|
||||||
|
(* 격리는 런타임의 일이지 언어의 일이 아니다. 언어에는 recover가 없고, *)
|
||||||
|
(* 러너는 죽은 것을 되살리는 것이 아니라 죽었다는 사실을 관찰하고 다음 *)
|
||||||
|
(* 으로 간다 — 프로세스 경계에 가깝다. *)
|
||||||
|
(* *)
|
||||||
|
(* 테스트가 effect-free임은 검사기가 이미 보장한다. 그래서 순서에 *)
|
||||||
|
(* 의존하지 않고, 어떤 순서로 돌려도 같다. *)
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
type test_result = {
|
||||||
|
t_module : string;
|
||||||
|
t_name : string;
|
||||||
|
t_pos : Token.pos;
|
||||||
|
t_failure : (Token.pos * string) option;
|
||||||
|
}
|
||||||
|
|
||||||
|
let run_tests ?(filter = "") (prog : Ir.program) : test_result list =
|
||||||
|
let st = { prog } in
|
||||||
|
List.rev prog.Ir.tests
|
||||||
|
|> List.filter (fun (_, name, _, _) ->
|
||||||
|
filter = "" || find_sub name filter <> None)
|
||||||
|
|> List.map (fun (m, name, pos, body) ->
|
||||||
|
let failure =
|
||||||
|
try
|
||||||
|
ignore (eval st [ [] ] body);
|
||||||
|
None
|
||||||
|
with
|
||||||
|
| Fail (p, msg) -> Some (p, msg)
|
||||||
|
| Return_exc _ -> None
|
||||||
|
in
|
||||||
|
{ t_module = m; t_name = name; t_pos = pos; t_failure = failure })
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
(* 얇은 typed IR.
|
||||||
|
|
||||||
|
왜 AST를 직접 해석하지 않는가 — 표면 문법이 실행 의미에 닿지 않는다는 것을
|
||||||
|
구조로 강제하기 위해서다. v1이 백엔드를 무엇으로 바꾸든 소비하는 것은 이
|
||||||
|
IR이고, 문법을 고쳐도 여기가 그대로면 실행 의미는 그대로다.
|
||||||
|
|
||||||
|
얇다는 것의 뜻: 새 개념을 만들지 않는다. 검사 단계가 이미 답한 질문을
|
||||||
|
다시 묻지 않는다 — 여기 도달한 프로그램은 타입, effect, capability,
|
||||||
|
ownership 검사를 모두 통과했다. 그래서 IR에는 타입 검사가 없다.
|
||||||
|
|
||||||
|
낮추기에서 사라지는 것:
|
||||||
|
- E_try: Result에 대한 match로 펼친다. `?`는 설탕이다
|
||||||
|
- E_inst: 타입 인자는 실행에 영향이 없다 (단형화 없음, 값 표현이 같다)
|
||||||
|
- 한정 이름: "Alias.f" 하나의 이름으로 평탄화된다 (검사 단계와 같은 규칙) *)
|
||||||
|
|
||||||
|
type pos = Token.pos
|
||||||
|
|
||||||
|
(* 이름은 낮추기 시점에 분류된다. 실행 중에 "이게 지역인가 전역인가"를
|
||||||
|
다시 묻지 않는다. *)
|
||||||
|
(* 전역 이름은 정규화된다: "<모듈 경로>#<이름>". 별칭은 가져오는 쪽의 선택이라
|
||||||
|
실행 의미에 들어와서는 안 된다 — 같은 함수가 부르는 자리마다 다른 이름이 되면
|
||||||
|
IR은 더 이상 v1의 번역 대상이 아니다. *)
|
||||||
|
type ref_kind =
|
||||||
|
| R_local of string
|
||||||
|
| R_global of string (* "<경로>#<이름>" *)
|
||||||
|
| R_ctor of string * string (* enum 이름, 정규화된 variant 이름 *)
|
||||||
|
| R_builtin of string (* 본문 없는 선언 = 런타임이 구현한다 *)
|
||||||
|
|
||||||
|
type pat =
|
||||||
|
| IP_wild
|
||||||
|
| IP_lit of Ast.lit
|
||||||
|
| IP_bind of string
|
||||||
|
| IP_ctor of string * pat list (* variant 이름 *)
|
||||||
|
|
||||||
|
type t =
|
||||||
|
| I_unit
|
||||||
|
| I_lit of Ast.lit
|
||||||
|
| I_ref of ref_kind * pos
|
||||||
|
| I_list of t list
|
||||||
|
| I_make of string * (string * t) list (* struct 생성 *)
|
||||||
|
| I_closure of closure
|
||||||
|
| I_if of { cond : t; then_ : t; else_ : t }
|
||||||
|
| I_match of { scrutinee : t; arms : (pat * t) list; pos : pos }
|
||||||
|
| I_scope of { name : string; body : t; pos : pos }
|
||||||
|
| I_seq of stmt list * t (* 블록: 문 나열 + 꼬리 값 *)
|
||||||
|
| I_call of { callee : t; args : t list; pos : pos }
|
||||||
|
| I_field of { obj : t; name : string; pos : pos }
|
||||||
|
| I_crash of t * pos
|
||||||
|
| I_unary of Ast.unop * t * pos
|
||||||
|
| I_binary of Ast.binop * t * t * pos
|
||||||
|
|
||||||
|
and stmt =
|
||||||
|
| S_let of pat * t * pos
|
||||||
|
| S_assign of { place : t; value : t; pos : pos }
|
||||||
|
| S_return of t * pos
|
||||||
|
| S_do of t
|
||||||
|
|
||||||
|
and closure = { c_params : string list; c_body : t; c_pos : pos }
|
||||||
|
|
||||||
|
type fn = { fn_name : string; fn_params : string list; fn_body : t }
|
||||||
|
|
||||||
|
type program = {
|
||||||
|
fns : (string, fn) Hashtbl.t;
|
||||||
|
(* 테스트. (모듈 경로, 이름, 위치, 본문) *)
|
||||||
|
mutable tests : (string * string * pos * t) list;
|
||||||
|
(* variant 이름 -> (enum 이름, 인자 개수) *)
|
||||||
|
ctors : (string, string * int) Hashtbl.t;
|
||||||
|
caps : (string, string list) Hashtbl.t; (* capability -> 메서드 이름 *)
|
||||||
|
consts : (string, t) Hashtbl.t;
|
||||||
|
}
|
||||||
|
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
(* 낮추기 *)
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
let qual modl name = match modl with Some a -> a ^ "." ^ name | None -> name
|
||||||
|
|
||||||
|
(* 지역 이름 스택. 검사 단계가 아니라 분류만 한다 — 여기서 못 찾은 이름은
|
||||||
|
전역이거나 생성자이거나 런타임 제공이다. *)
|
||||||
|
type modinfo = {
|
||||||
|
m_path : string; (* 정규 경로 *)
|
||||||
|
m_ast : Ast.modul;
|
||||||
|
m_deps : (string * string) list; (* 별칭 -> 정규 경로 *)
|
||||||
|
}
|
||||||
|
|
||||||
|
(* 본문 없는 선언은 런타임이 구현한다. 그 이름은 모듈 파일 이름에서 온다:
|
||||||
|
std/list.cool의 each는 "list.each"다. 가져오는 쪽의 별칭과 무관하다. *)
|
||||||
|
let builtin_name path name =
|
||||||
|
Filename.remove_extension (Filename.basename path) ^ "." ^ name
|
||||||
|
|
||||||
|
type lctx = {
|
||||||
|
prog : program;
|
||||||
|
mods : (string, modinfo) Hashtbl.t;
|
||||||
|
cur : modinfo;
|
||||||
|
mutable locals : string list list;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lpush c = c.locals <- [] :: c.locals
|
||||||
|
let lpop c = match c.locals with _ :: r -> c.locals <- r | [] -> ()
|
||||||
|
|
||||||
|
let lbind c n =
|
||||||
|
match c.locals with
|
||||||
|
| s :: r -> c.locals <- (n :: s) :: r
|
||||||
|
| [] -> c.locals <- [ [ n ] ]
|
||||||
|
|
||||||
|
let is_local c n = List.exists (fun s -> List.mem n s) c.locals
|
||||||
|
let builtin_ctors = [ "Ok"; "Err"; "Some"; "None" ]
|
||||||
|
|
||||||
|
(* 한 모듈 안에서 이름 하나를 분류한다. 지역이 아니면 그 모듈의 정의를 본다. *)
|
||||||
|
let in_module c (mi : modinfo) name =
|
||||||
|
let key = mi.m_path ^ "#" ^ name in
|
||||||
|
if Hashtbl.mem c.prog.ctors key then
|
||||||
|
match Hashtbl.find_opt c.prog.ctors key with
|
||||||
|
| Some (enum, _) -> Some (R_ctor (enum, key))
|
||||||
|
| None -> None
|
||||||
|
else if Hashtbl.mem c.prog.consts key then Some (R_global key)
|
||||||
|
else
|
||||||
|
let decl =
|
||||||
|
List.find_map
|
||||||
|
(function
|
||||||
|
| Ast.I_fn { decl; _ } when decl.fn_name = name -> Some decl
|
||||||
|
| _ -> None)
|
||||||
|
mi.m_ast.items
|
||||||
|
in
|
||||||
|
match decl with
|
||||||
|
| Some d when d.fn_body <> None -> Some (R_global key)
|
||||||
|
| Some _ -> Some (R_builtin (builtin_name mi.m_path name))
|
||||||
|
| None -> None
|
||||||
|
|
||||||
|
let classify c name =
|
||||||
|
if is_local c name then R_local name
|
||||||
|
else if List.mem name builtin_ctors then
|
||||||
|
R_ctor ((if name = "Ok" || name = "Err" then "Result" else "Option"), name)
|
||||||
|
else
|
||||||
|
match in_module c c.cur name with
|
||||||
|
| Some k -> k
|
||||||
|
| None -> (
|
||||||
|
(* 한정 이름 "A.f": A가 이 모듈의 import면 그 모듈에서 찾는다. *)
|
||||||
|
match String.index_opt name '.' with
|
||||||
|
| None -> R_builtin name
|
||||||
|
| Some i -> (
|
||||||
|
let a = String.sub name 0 i in
|
||||||
|
let f = String.sub name (i + 1) (String.length name - i - 1) in
|
||||||
|
match List.assoc_opt a c.cur.m_deps with
|
||||||
|
| None -> R_builtin name
|
||||||
|
| Some path -> (
|
||||||
|
match Hashtbl.find_opt c.mods path with
|
||||||
|
| None -> R_builtin name
|
||||||
|
| Some mi -> (
|
||||||
|
match in_module c mi f with
|
||||||
|
| Some k -> k
|
||||||
|
| None -> R_builtin (builtin_name path f)))))
|
||||||
|
|
||||||
|
(* 패턴의 생성자도 정규 이름으로 접는다. 값 쪽과 같은 키를 써야 match가
|
||||||
|
성립한다 — 두 곳이 다른 규칙을 쓰면 조용히 안 맞는다. *)
|
||||||
|
let ctor_key c modl name : string option =
|
||||||
|
if List.mem name builtin_ctors then Some name
|
||||||
|
else
|
||||||
|
let path =
|
||||||
|
match modl with
|
||||||
|
| None -> Some c.cur.m_path
|
||||||
|
| Some a -> List.assoc_opt a c.cur.m_deps
|
||||||
|
in
|
||||||
|
match path with
|
||||||
|
| None -> None
|
||||||
|
| Some p ->
|
||||||
|
let k = p ^ "#" ^ name in
|
||||||
|
if Hashtbl.mem c.prog.ctors k then Some k else None
|
||||||
|
|
||||||
|
let rec lower_pat c (p : Ast.pattern) : pat =
|
||||||
|
match p with
|
||||||
|
| Ast.P_wild _ -> IP_wild
|
||||||
|
| Ast.P_lit (l, _) -> IP_lit l
|
||||||
|
| Ast.P_bind (n, _) -> (
|
||||||
|
(* 인자 없는 생성자는 이름만 쓴다. 바인딩과 구별은 여기서 끝난다. *)
|
||||||
|
match ctor_key c None n with
|
||||||
|
| Some k -> IP_ctor (k, [])
|
||||||
|
| None ->
|
||||||
|
lbind c n;
|
||||||
|
IP_bind n)
|
||||||
|
| Ast.P_ctor { modl; name; args; _ } ->
|
||||||
|
let k =
|
||||||
|
match ctor_key c modl name with Some k -> k | None -> qual modl name
|
||||||
|
in
|
||||||
|
IP_ctor (k, List.map (lower_pat c) args)
|
||||||
|
|
||||||
|
let rec lower c (e : Ast.expr) : t =
|
||||||
|
match e with
|
||||||
|
| Ast.E_crash { msg; pos } -> I_crash (lower c msg, pos)
|
||||||
|
| Ast.E_lit (l, _) -> I_lit l
|
||||||
|
| Ast.E_ident (n, pos) -> I_ref (classify c n, pos)
|
||||||
|
| Ast.E_list (xs, _) -> I_list (List.map (lower c) xs)
|
||||||
|
| Ast.E_struct { name; fields; _ } ->
|
||||||
|
I_make (name, List.map (fun (n, e) -> (n, lower c e)) fields)
|
||||||
|
| Ast.E_closure cl ->
|
||||||
|
lpush c;
|
||||||
|
List.iter (fun (p : Ast.cl_param) -> lbind c p.cp_name) cl.cl_params;
|
||||||
|
let body = lower_block c cl.cl_body in
|
||||||
|
lpop c;
|
||||||
|
I_closure
|
||||||
|
{
|
||||||
|
c_params = List.map (fun (p : Ast.cl_param) -> p.cp_name) cl.cl_params;
|
||||||
|
c_body = body;
|
||||||
|
c_pos = cl.cl_pos;
|
||||||
|
}
|
||||||
|
| Ast.E_if { cond; then_; else_; _ } ->
|
||||||
|
let cond = lower c cond in
|
||||||
|
lpush c;
|
||||||
|
let t = lower_block c then_ in
|
||||||
|
lpop c;
|
||||||
|
let e =
|
||||||
|
match else_ with
|
||||||
|
| None -> I_unit
|
||||||
|
| Some e ->
|
||||||
|
lpush c;
|
||||||
|
let v = lower c e in
|
||||||
|
lpop c;
|
||||||
|
v
|
||||||
|
in
|
||||||
|
I_if { cond; then_ = t; else_ = e }
|
||||||
|
| Ast.E_match { scrutinee; arms; pos } ->
|
||||||
|
let s = lower c scrutinee in
|
||||||
|
let arms =
|
||||||
|
List.map
|
||||||
|
(fun (a : Ast.arm) ->
|
||||||
|
lpush c;
|
||||||
|
let p = lower_pat c a.arm_pat in
|
||||||
|
let b = lower c a.arm_body in
|
||||||
|
lpop c;
|
||||||
|
(p, b))
|
||||||
|
arms
|
||||||
|
in
|
||||||
|
I_match { scrutinee = s; arms; pos }
|
||||||
|
| Ast.E_scope { name; body; pos; _ } ->
|
||||||
|
lpush c;
|
||||||
|
lbind c name;
|
||||||
|
let b = lower_block c body in
|
||||||
|
lpop c;
|
||||||
|
I_scope { name; body = b; pos }
|
||||||
|
| Ast.E_block b ->
|
||||||
|
lpush c;
|
||||||
|
let v = lower_block c b in
|
||||||
|
lpop c;
|
||||||
|
v
|
||||||
|
| Ast.E_call { callee; args; pos } ->
|
||||||
|
I_call { callee = lower c callee; args = List.map (lower c) args; pos }
|
||||||
|
| Ast.E_field { obj; name; pos } -> (
|
||||||
|
(* Alias.f / String.concat 처럼 이름공간 접근은 하나의 이름으로 접는다.
|
||||||
|
값의 필드 접근과 구별되는 지점은 obj가 지역 이름이 아닌 것뿐이다. *)
|
||||||
|
match obj with
|
||||||
|
| Ast.E_ident (o, _) when not (is_local c o) ->
|
||||||
|
I_ref (classify c (o ^ "." ^ name), pos)
|
||||||
|
| _ -> I_field { obj = lower c obj; name; pos })
|
||||||
|
| Ast.E_inst { callee; _ } -> lower c callee
|
||||||
|
| Ast.E_try { inner; pos } ->
|
||||||
|
(* `?`는 설탕이다: Ok(v) => v, Err(e) => return Err(e) *)
|
||||||
|
I_match
|
||||||
|
{
|
||||||
|
scrutinee = lower c inner;
|
||||||
|
arms =
|
||||||
|
[
|
||||||
|
(IP_ctor ("Ok", [ IP_bind "?v" ]), I_ref (R_local "?v", pos));
|
||||||
|
( IP_ctor ("Err", [ IP_bind "?e" ]),
|
||||||
|
I_seq
|
||||||
|
( [
|
||||||
|
S_return
|
||||||
|
( I_call
|
||||||
|
{
|
||||||
|
callee = I_ref (R_ctor ("Result", "Err"), pos);
|
||||||
|
args = [ I_ref (R_local "?e", pos) ];
|
||||||
|
pos;
|
||||||
|
},
|
||||||
|
pos );
|
||||||
|
],
|
||||||
|
I_unit ) );
|
||||||
|
];
|
||||||
|
pos;
|
||||||
|
}
|
||||||
|
| Ast.E_unary { op; operand; pos } -> I_unary (op, lower c operand, pos)
|
||||||
|
| Ast.E_binary { op; lhs; rhs; pos } ->
|
||||||
|
I_binary (op, lower c lhs, lower c rhs, pos)
|
||||||
|
|
||||||
|
and lower_block c (b : Ast.block) : t =
|
||||||
|
let rec go = function
|
||||||
|
| [] -> I_unit
|
||||||
|
| [ Ast.S_expr e ] -> lower c e
|
||||||
|
| s :: rest -> (
|
||||||
|
let s = lower_stmt c s in
|
||||||
|
let tail = go rest in
|
||||||
|
match tail with
|
||||||
|
| I_seq (ss, t) -> I_seq (s :: ss, t)
|
||||||
|
| t -> I_seq ([ s ], t))
|
||||||
|
in
|
||||||
|
go b.stmts
|
||||||
|
|
||||||
|
and lower_stmt c (s : Ast.stmt) : stmt =
|
||||||
|
match s with
|
||||||
|
| Ast.S_let { pat; value; pos; _ } ->
|
||||||
|
let v = lower c value in
|
||||||
|
(* 값을 먼저 낮춘다 — 바인딩은 그 뒤에야 보인다 *)
|
||||||
|
S_let (lower_pat c pat, v, pos)
|
||||||
|
| Ast.S_return { value; pos } ->
|
||||||
|
S_return ((match value with Some e -> lower c e | None -> I_unit), pos)
|
||||||
|
| Ast.S_assign { place; value; pos } ->
|
||||||
|
S_assign { place = lower c place; value = lower c value; pos }
|
||||||
|
| Ast.S_expr e -> S_do (lower c e)
|
||||||
|
|
||||||
|
(* 모듈 그래프 전체를 하나의 IR 프로그램으로 낮춘다.
|
||||||
|
|
||||||
|
모듈 하나씩 낮추고 싶은 유혹이 있지만, 그러면 별칭이 실행 의미에 남는다.
|
||||||
|
같은 함수가 A에서는 Shapes.double, B에서는 Geo.double이 되어 IR이 더 이상
|
||||||
|
프로그램의 정규형이 아니게 된다. 정규 경로로 한 번 접는다. *)
|
||||||
|
let of_program (mods : modinfo list) : program =
|
||||||
|
let prog =
|
||||||
|
{
|
||||||
|
fns = Hashtbl.create 64;
|
||||||
|
tests = [];
|
||||||
|
ctors = Hashtbl.create 64;
|
||||||
|
caps = Hashtbl.create 16;
|
||||||
|
consts = Hashtbl.create 16;
|
||||||
|
}
|
||||||
|
in
|
||||||
|
let index = Hashtbl.create 16 in
|
||||||
|
List.iter (fun mi -> Hashtbl.replace index mi.m_path mi) mods;
|
||||||
|
(* 1차: 이름부터. 낮추기가 이름을 분류하려면 그래프 전체가 먼저 보여야 한다. *)
|
||||||
|
List.iter
|
||||||
|
(fun mi ->
|
||||||
|
let key n = mi.m_path ^ "#" ^ n in
|
||||||
|
List.iter
|
||||||
|
(fun (it : Ast.item) ->
|
||||||
|
match it with
|
||||||
|
| Ast.I_enum { name; variants; _ } ->
|
||||||
|
List.iter
|
||||||
|
(fun (v : Ast.variant) ->
|
||||||
|
Hashtbl.replace prog.ctors (key v.v_name)
|
||||||
|
(name, List.length v.v_args))
|
||||||
|
variants
|
||||||
|
| Ast.I_capability { name; methods; _ } ->
|
||||||
|
Hashtbl.replace prog.caps name
|
||||||
|
(List.map (fun (d : Ast.fn_decl) -> d.fn_name) methods)
|
||||||
|
| Ast.I_const { name; _ } ->
|
||||||
|
Hashtbl.replace prog.consts (key name) I_unit
|
||||||
|
| _ -> ())
|
||||||
|
mi.m_ast.items)
|
||||||
|
mods;
|
||||||
|
List.iter
|
||||||
|
(fun (v, e, n) -> Hashtbl.replace prog.ctors v (e, n))
|
||||||
|
[
|
||||||
|
("Ok", "Result", 1);
|
||||||
|
("Err", "Result", 1);
|
||||||
|
("Some", "Option", 1);
|
||||||
|
("None", "Option", 0);
|
||||||
|
];
|
||||||
|
(* 2차: 본문 *)
|
||||||
|
List.iter
|
||||||
|
(fun mi ->
|
||||||
|
let c = { prog; mods = index; cur = mi; locals = [] } in
|
||||||
|
let key n = mi.m_path ^ "#" ^ n in
|
||||||
|
List.iter
|
||||||
|
(fun (it : Ast.item) ->
|
||||||
|
match it with
|
||||||
|
| Ast.I_fn { decl; _ } -> (
|
||||||
|
match decl.fn_body with
|
||||||
|
| None -> ()
|
||||||
|
| Some body ->
|
||||||
|
c.locals <- [];
|
||||||
|
lpush c;
|
||||||
|
List.iter
|
||||||
|
(fun (p : Ast.param) -> lbind c p.p_name)
|
||||||
|
decl.fn_params;
|
||||||
|
let b = lower_block c body in
|
||||||
|
lpop c;
|
||||||
|
Hashtbl.replace prog.fns (key decl.fn_name)
|
||||||
|
{
|
||||||
|
fn_name = key decl.fn_name;
|
||||||
|
fn_params =
|
||||||
|
List.map
|
||||||
|
(fun (p : Ast.param) -> p.p_name)
|
||||||
|
decl.fn_params;
|
||||||
|
fn_body = b;
|
||||||
|
})
|
||||||
|
| Ast.I_const { name; value; _ } ->
|
||||||
|
c.locals <- [];
|
||||||
|
Hashtbl.replace prog.consts (key name) (lower c value)
|
||||||
|
| Ast.I_test { name; body; pos } ->
|
||||||
|
c.locals <- [];
|
||||||
|
lpush c;
|
||||||
|
let b = lower_block c body in
|
||||||
|
lpop c;
|
||||||
|
prog.tests <- (mi.m_path, name, pos, b) :: prog.tests
|
||||||
|
| _ -> ())
|
||||||
|
mi.m_ast.items)
|
||||||
|
mods;
|
||||||
|
prog
|
||||||
@@ -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;
|
||||||
|
]
|
||||||
+675
@@ -0,0 +1,675 @@
|
|||||||
|
(* move / affinity 검사.
|
||||||
|
|
||||||
|
보안 정리 (ii) — safe code에서 capability는 복제·위조되지 않는다 — 를 코드로
|
||||||
|
닫는 단계다. 검사는 전부 함수 로컬 데이터플로우다. 전역 분석이 없다.
|
||||||
|
|
||||||
|
affinity의 뿌리는 capability다. capability를 필드로 가진 타입은 전이적으로
|
||||||
|
affine이고(철학 3), 이 전이가 없으면 wrapper 하나를 복사해 capability가
|
||||||
|
사실상 복제된다.
|
||||||
|
|
||||||
|
외부 타입은 affine임을 증명할 수 없으므로 copyable로 본다 — 모르는 것을
|
||||||
|
위반이라고 말하지 않는다. 그래서 이 검사를 시험하려면 자원 타입을 모듈
|
||||||
|
안에서 정의해야 한다 (samples/05). *)
|
||||||
|
|
||||||
|
open Ast
|
||||||
|
|
||||||
|
(* lint는 오류가 아니다. 코드는 뜻이 통하고 검사도 계속되어야 한다 —
|
||||||
|
Resolve와 같은 구분이다 *)
|
||||||
|
type error = { pos : Token.pos; msg : string; lint : bool }
|
||||||
|
|
||||||
|
(* 값이 무엇인가: 소유한 affine 값인가, 빌린 값인가. 둘은 직교한다. *)
|
||||||
|
type vinfo = { v_affine : bool; v_use : bool }
|
||||||
|
|
||||||
|
let v_copy = { v_affine = false; v_use = false }
|
||||||
|
|
||||||
|
type binding = {
|
||||||
|
b_id : int;
|
||||||
|
b_name : string;
|
||||||
|
b_affine : bool;
|
||||||
|
b_use : bool;
|
||||||
|
b_mut : bool;
|
||||||
|
b_depth : int;
|
||||||
|
(* 선언된 타입. 필드가 affine인지 알려면 필요하다 — 모르면 필드에서
|
||||||
|
꺼내는 것을 막을 수 없고, 그러면 capability가 struct를 통해 복제된다. *)
|
||||||
|
b_ty : ty option;
|
||||||
|
}
|
||||||
|
|
||||||
|
type fninfo = { f_self : bool; f_params : param list; f_ret : Ast.ty option }
|
||||||
|
|
||||||
|
type state = {
|
||||||
|
aff : (string, bool) Hashtbl.t;
|
||||||
|
(* struct 이름 -> 필드 이름과 타입 *)
|
||||||
|
fields : (string, (string * ty) list) Hashtbl.t;
|
||||||
|
fns : (string, fninfo) Hashtbl.t;
|
||||||
|
(* 열거형 생성자 이름. 생성자는 인자를 소유한다 — 값이 variant 안으로
|
||||||
|
들어가기 때문이다. 이것이 없으면 Some(f)를 두 번 써서 affine 값을
|
||||||
|
복제할 수 있다 (보안 정리 ii). 리스트 리터럴은 처음부터 막혀 있었고
|
||||||
|
struct 리터럴도 그랬는데, 생성자만 호출로 취급돼 빠져 있었다. *)
|
||||||
|
ctors : (string, unit) Hashtbl.t;
|
||||||
|
meths : (string, (string * fninfo) list) Hashtbl.t;
|
||||||
|
mutable scopes : binding list list;
|
||||||
|
moved : (int, Token.pos) Hashtbl.t;
|
||||||
|
mutable next_id : int;
|
||||||
|
mutable depth : int;
|
||||||
|
(* 클로저 프레임: (프레임 깊이, 잡아온 바깥 바인딩). 중첩 클로저를 위해 스택 *)
|
||||||
|
mutable frames : (int * binding list ref) list;
|
||||||
|
(* 클로저 인자를 걸을 때 기대 파라미터 타입. move 검사는 타입 검사와 별도
|
||||||
|
순회라 타입을 모르는데, 클로저 파라미터의 affinity를 알아야 빌림 여부를
|
||||||
|
판정할 수 있다. 호출 대상의 시그니처에서 읽어와 여기 잠깐 둔다. *)
|
||||||
|
mutable cl_expect : fn_param_ty list option;
|
||||||
|
mutable errors : error list;
|
||||||
|
}
|
||||||
|
|
||||||
|
let err st pos msg = st.errors <- { pos; msg; lint = false } :: st.errors
|
||||||
|
let lint st pos msg = st.errors <- { pos; msg; lint = true } :: st.errors
|
||||||
|
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
(* affinity 유도 *)
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
let builtin_containers = [ "List"; "Option"; "Result" ]
|
||||||
|
|
||||||
|
let rec ty_affine st (t : Ast.ty) =
|
||||||
|
match t with
|
||||||
|
| T_fn { affine; _ } -> affine
|
||||||
|
| T_named { modl; name; args; _ } ->
|
||||||
|
let name = match modl with Some a -> a ^ "." ^ name | None -> name in
|
||||||
|
let self =
|
||||||
|
match Hashtbl.find_opt st.aff name with Some b -> b | None -> false
|
||||||
|
in
|
||||||
|
let arg_affine =
|
||||||
|
List.exists
|
||||||
|
(function TA_ty t -> ty_affine st t | TA_eff _ -> false)
|
||||||
|
args
|
||||||
|
in
|
||||||
|
self || (List.mem name builtin_containers && arg_affine) || arg_affine
|
||||||
|
|
||||||
|
(* capability가 뿌리다. struct/enum은 필드에서 전이된다. 상호 재귀 타입을 위해
|
||||||
|
변화가 없을 때까지 돈다. *)
|
||||||
|
let derive_affinity st (items : item list) =
|
||||||
|
List.iter
|
||||||
|
(fun it ->
|
||||||
|
match it with
|
||||||
|
| I_capability { name; _ } -> Hashtbl.replace st.aff name true
|
||||||
|
| I_struct { name; fields; _ } ->
|
||||||
|
Hashtbl.replace st.aff name false;
|
||||||
|
Hashtbl.replace st.fields name
|
||||||
|
(List.map (fun f -> (f.f_name, f.f_ty)) fields)
|
||||||
|
| I_enum { name; _ } -> Hashtbl.replace st.aff name false
|
||||||
|
| _ -> ())
|
||||||
|
items;
|
||||||
|
let changed = ref true in
|
||||||
|
while !changed do
|
||||||
|
changed := false;
|
||||||
|
List.iter
|
||||||
|
(fun it ->
|
||||||
|
let update name affine =
|
||||||
|
if affine && Hashtbl.find_opt st.aff name <> Some true then (
|
||||||
|
Hashtbl.replace st.aff name true;
|
||||||
|
changed := true)
|
||||||
|
in
|
||||||
|
match it with
|
||||||
|
| I_struct { name; fields; _ } ->
|
||||||
|
update name (List.exists (fun f -> ty_affine st f.f_ty) fields)
|
||||||
|
| I_enum { name; variants; _ } ->
|
||||||
|
update name
|
||||||
|
(List.exists
|
||||||
|
(fun v -> List.exists (ty_affine st) v.v_args)
|
||||||
|
variants)
|
||||||
|
| _ -> ())
|
||||||
|
items
|
||||||
|
done;
|
||||||
|
(* copyable 선언과 affine 필드는 공존할 수 없다 *)
|
||||||
|
List.iter
|
||||||
|
(fun it ->
|
||||||
|
match it with
|
||||||
|
| I_struct { copyable = true; name; fields; pos; _ } ->
|
||||||
|
List.iter
|
||||||
|
(fun f ->
|
||||||
|
if ty_affine st f.f_ty then
|
||||||
|
err st f.f_pos
|
||||||
|
(Printf.sprintf "%s은(는) copyable로 선언되었지만 %s 필드가 affine입니다"
|
||||||
|
name f.f_name))
|
||||||
|
fields;
|
||||||
|
ignore pos
|
||||||
|
| _ -> ())
|
||||||
|
items
|
||||||
|
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
(* 스코프 *)
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
let push st = st.scopes <- [] :: st.scopes
|
||||||
|
let pop st = match st.scopes with _ :: r -> st.scopes <- r | [] -> ()
|
||||||
|
|
||||||
|
let add st name ~affine ~use ~mut_ ?ty () =
|
||||||
|
st.next_id <- st.next_id + 1;
|
||||||
|
let b =
|
||||||
|
{
|
||||||
|
b_id = st.next_id;
|
||||||
|
b_name = name;
|
||||||
|
b_affine = affine;
|
||||||
|
b_use = use;
|
||||||
|
b_mut = mut_;
|
||||||
|
b_depth = st.depth;
|
||||||
|
b_ty = ty;
|
||||||
|
}
|
||||||
|
in
|
||||||
|
(match st.scopes with
|
||||||
|
| s :: r -> st.scopes <- (b :: s) :: r
|
||||||
|
| [] -> st.scopes <- [ [ b ] ]);
|
||||||
|
b
|
||||||
|
|
||||||
|
let find st name =
|
||||||
|
let rec go = function
|
||||||
|
| [] -> None
|
||||||
|
| s :: r -> (
|
||||||
|
match List.find_opt (fun b -> b.b_name = name) s with
|
||||||
|
| Some b -> Some b
|
||||||
|
| None -> go r)
|
||||||
|
in
|
||||||
|
go st.scopes
|
||||||
|
|
||||||
|
(* 클로저 안에서 바깥 바인딩을 건드리면 capture다. 프레임마다 기록한다. *)
|
||||||
|
let note_capture st b =
|
||||||
|
List.iter
|
||||||
|
(fun (fdepth, acc) ->
|
||||||
|
if b.b_depth < fdepth && not (List.exists (fun x -> x.b_id = b.b_id) !acc)
|
||||||
|
then acc := b :: !acc)
|
||||||
|
st.frames
|
||||||
|
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
(* 분기 병합 — 보수적 합집합 *)
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
let snapshot st = Hashtbl.copy st.moved
|
||||||
|
|
||||||
|
let restore st snap =
|
||||||
|
Hashtbl.reset st.moved;
|
||||||
|
Hashtbl.iter (fun k v -> Hashtbl.replace st.moved k v) snap
|
||||||
|
|
||||||
|
let merge st snaps =
|
||||||
|
(* 한 분기에서라도 moved면 병합 지점 이후 moved *)
|
||||||
|
Hashtbl.reset st.moved;
|
||||||
|
List.iter
|
||||||
|
(fun snap ->
|
||||||
|
Hashtbl.iter
|
||||||
|
(fun k v ->
|
||||||
|
if not (Hashtbl.mem st.moved k) then Hashtbl.replace st.moved k v)
|
||||||
|
snap)
|
||||||
|
snaps
|
||||||
|
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
(* 식 *)
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
type ctx = Borrow | Move of string (* 어디로 옮겨가는지 — 진단에 쓴다 *)
|
||||||
|
|
||||||
|
(* 필드의 선언된 타입. 바인딩의 타입을 알아야 찾을 수 있다 — 모르면 None이고,
|
||||||
|
그때는 객체가 affine인지로 보수적으로 판정한다. *)
|
||||||
|
let field_ty st (obj : expr) (name : string) : ty option =
|
||||||
|
match obj with
|
||||||
|
| E_ident (n, _) -> (
|
||||||
|
match find st n with
|
||||||
|
| Some b -> (
|
||||||
|
match b.b_ty with
|
||||||
|
| Some (T_named { name = tn; _ }) -> (
|
||||||
|
match Hashtbl.find_opt st.fields tn with
|
||||||
|
| Some fs -> List.assoc_opt name fs
|
||||||
|
| None -> None)
|
||||||
|
| _ -> None)
|
||||||
|
| None -> None)
|
||||||
|
| _ -> None
|
||||||
|
|
||||||
|
(* 지금 살아 있는, 소유한 affine 바인딩. 소유했다(use가 아니다)는 것은
|
||||||
|
정리할 책임이 이 함수에 있다는 뜻이고, moved가 아니라는 것은 아직
|
||||||
|
아무 데도 넘기지 않았다는 뜻이다. *)
|
||||||
|
let live_owned st =
|
||||||
|
List.concat_map
|
||||||
|
(fun scope ->
|
||||||
|
List.filter
|
||||||
|
(fun b -> b.b_affine && (not b.b_use) && not (Hashtbl.mem st.moved b.b_id))
|
||||||
|
scope)
|
||||||
|
st.scopes
|
||||||
|
|
||||||
|
(* 조기 탈출 지점에서 새는 것을 말한다. 이름순으로 하나의 메시지에 모은다 —
|
||||||
|
자원 셋을 들고 있으면 같은 자리에 경고가 셋 뜨는 것이 아니라 하나 뜬다. *)
|
||||||
|
let live_leaks st pos what =
|
||||||
|
match List.sort compare (List.map (fun b -> b.b_name) (live_owned st)) with
|
||||||
|
| [] -> ()
|
||||||
|
| names ->
|
||||||
|
lint st pos
|
||||||
|
(Printf.sprintf "%s이(가) 여기서 빠져나가면 %s이(가) 정리되지 않습니다 (%s는 뒤의 정리를 건너뜁니다)"
|
||||||
|
what (String.concat ", " names) what)
|
||||||
|
|
||||||
|
let rec walk st (ctx : ctx) (e : expr) : vinfo =
|
||||||
|
match e with
|
||||||
|
| E_lit _ -> v_copy
|
||||||
|
(* crash는 돌아오지 않지만 메시지 식은 계산된다. 메시지는 빌려 쓴다 *)
|
||||||
|
| E_crash { msg; _ } ->
|
||||||
|
ignore (walk st Borrow msg);
|
||||||
|
v_copy
|
||||||
|
| E_ident (n, pos) -> (
|
||||||
|
match find st n with
|
||||||
|
| None -> v_copy
|
||||||
|
| Some b ->
|
||||||
|
note_capture st b;
|
||||||
|
(match Hashtbl.find_opt st.moved b.b_id with
|
||||||
|
| Some mp ->
|
||||||
|
err st pos
|
||||||
|
(Printf.sprintf "%s은(는) 이미 move되었습니다 (%d:%d에서 소비)" n
|
||||||
|
mp.Token.line mp.Token.col)
|
||||||
|
| None -> ());
|
||||||
|
(match ctx with
|
||||||
|
| Borrow -> ()
|
||||||
|
| Move where ->
|
||||||
|
if b.b_use then
|
||||||
|
err st pos
|
||||||
|
(Printf.sprintf "%s은(는) 빌린 값이라 %s 없습니다 (own으로 받아야 합니다)" n
|
||||||
|
where)
|
||||||
|
else if b.b_affine then Hashtbl.replace st.moved b.b_id pos);
|
||||||
|
{ v_affine = b.b_affine; v_use = b.b_use })
|
||||||
|
| E_list (xs, _) ->
|
||||||
|
let infos = List.map (walk st (Move "컨테이너에 넣을 수")) xs in
|
||||||
|
{ v_affine = List.exists (fun i -> i.v_affine) infos; v_use = false }
|
||||||
|
| E_struct { name; fields; _ } ->
|
||||||
|
List.iter (fun (_, e) -> ignore (walk st (Move "struct에 저장할 수") e)) fields;
|
||||||
|
{
|
||||||
|
v_affine =
|
||||||
|
(match Hashtbl.find_opt st.aff name with
|
||||||
|
| Some b -> b
|
||||||
|
| None -> false);
|
||||||
|
v_use = false;
|
||||||
|
}
|
||||||
|
| E_closure c -> (
|
||||||
|
let r = walk_closure st c in
|
||||||
|
(* use의 전염: 빌린 값을 capture한 클로저는 그 자체가 빌린 값이라
|
||||||
|
소유를 가져가는 자리로 갈 수 없다. 별도의 nonescaping 개념 없이
|
||||||
|
use 규칙 하나로 닫힌다. *)
|
||||||
|
match ctx with
|
||||||
|
| Move where when r.v_use ->
|
||||||
|
err st c.cl_pos
|
||||||
|
(Printf.sprintf "빌린 값을 capture한 클로저는 %s 없습니다 (use 값은 탈출하지 못합니다)"
|
||||||
|
where);
|
||||||
|
r
|
||||||
|
| _ -> r)
|
||||||
|
| E_if { cond; then_; else_; _ } ->
|
||||||
|
ignore (walk st Borrow cond);
|
||||||
|
let before = snapshot st in
|
||||||
|
let t1 = walk_block st ctx then_ in
|
||||||
|
let s1 = snapshot st in
|
||||||
|
restore st before;
|
||||||
|
let t2 = match else_ with None -> v_copy | Some e -> walk st ctx e in
|
||||||
|
let s2 = snapshot st in
|
||||||
|
merge st [ s1; s2 ];
|
||||||
|
{ v_affine = t1.v_affine || t2.v_affine; v_use = t1.v_use || t2.v_use }
|
||||||
|
| E_match { scrutinee; arms; _ } ->
|
||||||
|
let sinfo = walk st Borrow scrutinee in
|
||||||
|
(* 소유한 affine 값을 이름을 묶는 패턴으로 분해하면 그 값 자체가
|
||||||
|
소비된다. 조각이 밖으로 나가기 때문이다 — 그러지 않으면 같은
|
||||||
|
파이프를 두 번 분해해 같은 fd를 두 번 꺼낼 수 있다 (보안 정리 ii).
|
||||||
|
|
||||||
|
빌린 값은 여기서 건드리지 않는다. 빌린 scrutinee의 조각은 use로
|
||||||
|
묶이므로 꺼내려는 순간 각자의 자리에서 걸리고, 그쪽 진단이 더
|
||||||
|
정확하다. 필드에서 분해하는 것도 마찬가지로 E_field가 잡는다. *)
|
||||||
|
(if
|
||||||
|
sinfo.v_affine && (not sinfo.v_use)
|
||||||
|
&& List.exists (fun a -> pat_binds a.arm_pat) arms
|
||||||
|
then
|
||||||
|
match scrutinee with
|
||||||
|
| E_ident (n, spos) -> (
|
||||||
|
match find st n with
|
||||||
|
| Some b when b.b_affine && not b.b_use ->
|
||||||
|
if not (Hashtbl.mem st.moved b.b_id) then
|
||||||
|
Hashtbl.replace st.moved b.b_id spos
|
||||||
|
| _ -> ())
|
||||||
|
| _ -> ());
|
||||||
|
let before = snapshot st in
|
||||||
|
let results =
|
||||||
|
List.map
|
||||||
|
(fun a ->
|
||||||
|
restore st before;
|
||||||
|
push st;
|
||||||
|
bind_pattern st sinfo a.arm_pat;
|
||||||
|
let r = walk st ctx a.arm_body in
|
||||||
|
pop st;
|
||||||
|
(r, snapshot st))
|
||||||
|
arms
|
||||||
|
in
|
||||||
|
if results <> [] then merge st (List.map snd results);
|
||||||
|
{
|
||||||
|
v_affine = List.exists (fun (r, _) -> r.v_affine) results;
|
||||||
|
v_use = List.exists (fun (r, _) -> r.v_use) results;
|
||||||
|
}
|
||||||
|
| E_scope { name; parent; body; _ } ->
|
||||||
|
ignore (walk st Borrow (E_ident (parent, Token.{ line = 0; col = 0 })));
|
||||||
|
push st;
|
||||||
|
(* 자식 TaskScope는 second-class다 — 블록 밖으로 나갈 수 없다 *)
|
||||||
|
ignore (add st name ~affine:true ~use:true ~mut_:false ());
|
||||||
|
let r = walk_block st ctx body in
|
||||||
|
pop st;
|
||||||
|
r
|
||||||
|
| E_block b ->
|
||||||
|
push st;
|
||||||
|
let r = walk_block st ctx b in
|
||||||
|
pop st;
|
||||||
|
r
|
||||||
|
| E_call { callee; args; pos } -> walk_call st callee args pos
|
||||||
|
| E_field { obj; name; pos } ->
|
||||||
|
(* v0에 부분 move는 없다. 필드 접근은 빌림이고 결과도 빌린 값이다.
|
||||||
|
그러므로 Move 자리에 놓으면 오류다 — 이 검사가 없으면 capability를
|
||||||
|
struct에 넣고 필드를 두 번 읽어 복제할 수 있다 (보안 정리 ii). *)
|
||||||
|
let o = walk st Borrow obj in
|
||||||
|
let fa =
|
||||||
|
match field_ty st obj name with
|
||||||
|
| Some t -> ty_affine st t
|
||||||
|
| None -> o.v_affine || o.v_use
|
||||||
|
in
|
||||||
|
(match ctx with
|
||||||
|
| Borrow -> ()
|
||||||
|
| Move where ->
|
||||||
|
if fa then
|
||||||
|
err st pos
|
||||||
|
(Printf.sprintf
|
||||||
|
"%s 필드는 affine이라 %s 없습니다 (v0에는 부분 move가 없습니다 — 꺼내려면 열거형으로 \
|
||||||
|
감싸십시오)"
|
||||||
|
name where));
|
||||||
|
if fa then { v_affine = true; v_use = true } else v_copy
|
||||||
|
| E_inst { callee; _ } -> walk st Borrow callee
|
||||||
|
| E_try { inner; pos } ->
|
||||||
|
let r = walk st ctx inner in
|
||||||
|
(* ?는 여기서 함수를 빠져나갈 수 있다. 그 길에는 뒤에 오는 정리
|
||||||
|
코드가 없다. 지금 살아 있는, 소유한 affine 값은 그 길에서 샌다.
|
||||||
|
|
||||||
|
v0 정책은 "오용 금지, 누수 허용"이므로 이것은 오류가 아니라
|
||||||
|
lint다 — 검사를 막지 않고 사실만 말한다. 누수를 언어가 강제로
|
||||||
|
막을지는 `with`가 결정할 문제이고, 그 결정에는 "실제 코드에서
|
||||||
|
얼마나 자주 새는가"라는 측정이 필요하다. 이 lint가 그 측정이다. *)
|
||||||
|
live_leaks st pos "?";
|
||||||
|
r
|
||||||
|
| E_unary { operand; _ } -> walk st Borrow operand
|
||||||
|
| E_binary { lhs; rhs; _ } ->
|
||||||
|
ignore (walk st Borrow lhs);
|
||||||
|
ignore (walk st Borrow rhs);
|
||||||
|
v_copy
|
||||||
|
|
||||||
|
(* 이름을 묶는 자리가 하나라도 있는가. 있으면 분해로 값이 밖으로 나간다 *)
|
||||||
|
and pat_binds = function
|
||||||
|
| P_bind _ -> true
|
||||||
|
| P_ctor { args; _ } -> List.exists pat_binds args
|
||||||
|
| P_wild _ | P_lit _ -> false
|
||||||
|
|
||||||
|
and bind_pattern st (info : vinfo) p =
|
||||||
|
match p with
|
||||||
|
| P_wild _ | P_lit _ -> ()
|
||||||
|
| P_bind (n, _) ->
|
||||||
|
ignore (add st n ~affine:info.v_affine ~use:info.v_use ~mut_:false ())
|
||||||
|
| P_ctor { args; _ } -> List.iter (bind_pattern st info) args
|
||||||
|
|
||||||
|
and walk_block st ctx (b : block) : vinfo =
|
||||||
|
let rec go = function
|
||||||
|
| [] -> v_copy
|
||||||
|
| [ S_expr e ] -> walk st ctx e (* 꼬리 식은 블록의 값이다 *)
|
||||||
|
| s :: rest ->
|
||||||
|
walk_stmt st s;
|
||||||
|
go rest
|
||||||
|
in
|
||||||
|
go b.stmts
|
||||||
|
|
||||||
|
and walk_stmt st = function
|
||||||
|
| S_let { mut_; pat; value; _ } ->
|
||||||
|
let info = walk st (Move "다른 이름에 묶을 수") value in
|
||||||
|
let rec bind p =
|
||||||
|
match p with
|
||||||
|
| P_bind (n, _) ->
|
||||||
|
ignore (add st n ~affine:info.v_affine ~use:info.v_use ~mut_ ())
|
||||||
|
| P_ctor { args; _ } -> List.iter bind args
|
||||||
|
| _ -> ()
|
||||||
|
in
|
||||||
|
bind pat
|
||||||
|
| S_return { value; pos } ->
|
||||||
|
(match value with
|
||||||
|
| None -> ()
|
||||||
|
| Some e -> ignore (walk st (Move "반환할 수") e));
|
||||||
|
live_leaks st pos "return"
|
||||||
|
| S_assign { place; value; _ } ->
|
||||||
|
ignore (walk st (Move "대입할 수") value);
|
||||||
|
ignore (walk st Borrow place)
|
||||||
|
| S_expr e -> ignore (walk st Borrow e)
|
||||||
|
|
||||||
|
(* 클로저: 무엇을 잡아왔는지가 클로저 자신의 성질을 정한다 (전이 규칙) *)
|
||||||
|
and walk_closure st (c : closure) : vinfo =
|
||||||
|
st.depth <- st.depth + 1;
|
||||||
|
let acc = ref [] in
|
||||||
|
st.frames <- (st.depth, acc) :: st.frames;
|
||||||
|
push st;
|
||||||
|
let expect = st.cl_expect in
|
||||||
|
st.cl_expect <- None;
|
||||||
|
List.iteri
|
||||||
|
(fun i (p : cl_param) ->
|
||||||
|
let affine =
|
||||||
|
match p.cp_ty with
|
||||||
|
| Some t -> ty_affine st t
|
||||||
|
| None -> (
|
||||||
|
(* 표기가 없으면 호출 대상의 시그니처에서 읽어온다 *)
|
||||||
|
match Option.bind expect (fun ps -> List.nth_opt ps i) with
|
||||||
|
| Some pt -> ty_affine st pt.pt_ty
|
||||||
|
| None -> false)
|
||||||
|
in
|
||||||
|
(* 함수 파라미터와 같은 규칙 — 무표기는 빌림이다. 전에는 클로저
|
||||||
|
파라미터를 무조건 소유로 봤고, 그래서 고차 경계에서 소유권 검사가
|
||||||
|
뚫렸다 (dogfoods/FINDINGS D5). *)
|
||||||
|
let use_ = affine && not p.cp_own in
|
||||||
|
ignore (add st p.cp_name ~affine ~use:use_ ~mut_:false ?ty:p.cp_ty ()))
|
||||||
|
c.cl_params;
|
||||||
|
ignore (walk_block st (Move "반환할 수") c.cl_body);
|
||||||
|
pop st;
|
||||||
|
st.frames <- List.tl st.frames;
|
||||||
|
st.depth <- st.depth - 1;
|
||||||
|
let captured = !acc in
|
||||||
|
let affine = ref false and use_ = ref false in
|
||||||
|
List.iter
|
||||||
|
(fun b ->
|
||||||
|
if b.b_mut then
|
||||||
|
err st c.cl_pos
|
||||||
|
(Printf.sprintf
|
||||||
|
"클로저는 mut 바인딩 %s을(를) capture할 수 없습니다 (v0에 참조가 없으므로 별칭도 복사도 만들지 \
|
||||||
|
않는다)"
|
||||||
|
b.b_name);
|
||||||
|
if b.b_use then use_ := true;
|
||||||
|
if b.b_affine && not b.b_use then (
|
||||||
|
(* by-move capture: 바깥에서는 여기서 소비된다 *)
|
||||||
|
affine := true;
|
||||||
|
if not (Hashtbl.mem st.moved b.b_id) then
|
||||||
|
Hashtbl.replace st.moved b.b_id c.cl_pos))
|
||||||
|
captured;
|
||||||
|
{ v_affine = !affine; v_use = !use_ }
|
||||||
|
|
||||||
|
and is_ctor st callee =
|
||||||
|
match callee with
|
||||||
|
| E_ident (n, _) when find st n = None -> Hashtbl.mem st.ctors n
|
||||||
|
(* Alias.Variant — 가져온 모듈의 생성자 *)
|
||||||
|
| E_field { obj = E_ident (o, _); name; _ } when find st o = None ->
|
||||||
|
Hashtbl.mem st.ctors name || Hashtbl.mem st.ctors (o ^ "." ^ name)
|
||||||
|
| _ -> false
|
||||||
|
|
||||||
|
and walk_call st callee args pos =
|
||||||
|
let info = callee_info st callee in
|
||||||
|
if info = None && is_ctor st callee then begin
|
||||||
|
(* 생성자는 인자를 소유한다. 값이 variant 안으로 들어가기 때문이다 —
|
||||||
|
리스트 리터럴, struct 리터럴과 같은 규칙이다.
|
||||||
|
|
||||||
|
affinity는 넣은 것에서 전이된다. 선언된 타입이 아니라 실제로 넣은
|
||||||
|
값을 보는 것은 E_list와 같다. *)
|
||||||
|
let infos = List.map (walk st (Move "열거형에 넣을 수")) args in
|
||||||
|
{ v_affine = List.exists (fun i -> i.v_affine) infos; v_use = false }
|
||||||
|
end
|
||||||
|
else walk_plain_call st callee args pos info
|
||||||
|
|
||||||
|
and walk_plain_call st callee args pos info =
|
||||||
|
(* 수신자를 소비하는 메서드면 수신자가 Move 자리다. 이것이 없으면
|
||||||
|
f.close()를 두 번 부를 수 있다 — 그리고 자원이 자기 정리를 스스로
|
||||||
|
선언한다는 말 자체가 성립하지 않는다. *)
|
||||||
|
(match (callee, info) with
|
||||||
|
| E_field { obj; name; _ }, Some { f_self = true; _ } ->
|
||||||
|
ignore (walk st (Move (Printf.sprintf "%s()로 소비할 수" name)) obj)
|
||||||
|
| _ -> ignore (walk st Borrow callee));
|
||||||
|
let params = match info with Some f -> f.f_params | None -> [] in
|
||||||
|
List.iteri
|
||||||
|
(fun i a ->
|
||||||
|
let p = List.nth_opt params i in
|
||||||
|
let own = match p with Some p -> p.p_own | None -> false in
|
||||||
|
let pty = Option.map (fun p -> p.p_ty) p in
|
||||||
|
let ctx = if own then Move "다른 함수에 넘길 수" else Borrow in
|
||||||
|
(match (a, pty) with
|
||||||
|
| E_closure _, Some (T_fn { params; _ }) -> st.cl_expect <- Some params
|
||||||
|
| _ -> st.cl_expect <- None);
|
||||||
|
let got = walk st ctx a in
|
||||||
|
(* callable affinity: affine 클로저를 fn 자리에 넘길 수 없다 *)
|
||||||
|
match pty with
|
||||||
|
| Some (T_fn { affine = false; _ }) when got.v_affine && not got.v_use ->
|
||||||
|
err st pos
|
||||||
|
"affine 값을 capture한 클로저는 fn 자리에 넘길 수 없습니다 (affine fn이어야 합니다)"
|
||||||
|
| _ -> ())
|
||||||
|
args;
|
||||||
|
match info with
|
||||||
|
| Some { f_ret = Some t; _ } -> { v_affine = ty_affine st t; v_use = false }
|
||||||
|
| _ -> v_copy
|
||||||
|
|
||||||
|
and callee_info st callee =
|
||||||
|
match callee with
|
||||||
|
| E_ident (n, _) when find st n = None -> Hashtbl.find_opt st.fns n
|
||||||
|
| E_inst { callee = E_ident (n, _); _ } when find st n = None ->
|
||||||
|
Hashtbl.find_opt st.fns n
|
||||||
|
(* 모듈 별칭을 통한 호출: 가져온 함수는 "Alias.f" 키로 들어와 있다 *)
|
||||||
|
| E_field { obj = E_ident (o, _); name; _ } when find st o = None ->
|
||||||
|
Hashtbl.find_opt st.fns (o ^ "." ^ name)
|
||||||
|
(* 값의 메서드. 수신자가 중첩된 필드여도(w.pay.close()) 여기로 온다 —
|
||||||
|
예전에는 obj가 이름일 때만 봤고, 그래서 중첩 수신자는 시그니처를 못
|
||||||
|
찾아 조용히 빌림이 됐다. *)
|
||||||
|
| E_field { obj; name; _ } -> meth_info st obj name
|
||||||
|
| _ -> None
|
||||||
|
|
||||||
|
(* 수신자의 메서드 시그니처를 찾는다.
|
||||||
|
|
||||||
|
수신자의 선언된 타입을 알면 그 capability 안에서만 찾는다. 모르면 이름만
|
||||||
|
보고 아무 capability에서나 찾는다 — 이름이 겹치면 엉뚱한 시그니처를 읽고
|
||||||
|
진단이 부정확해질 수 있다. 통과가 느슨해지지는 않는다 (타입 검사가 수신자
|
||||||
|
타입을 이미 확인한다). *)
|
||||||
|
and meth_info st obj name =
|
||||||
|
let by_name () =
|
||||||
|
Hashtbl.fold
|
||||||
|
(fun _ methods acc ->
|
||||||
|
match acc with Some _ -> acc | None -> List.assoc_opt name methods)
|
||||||
|
st.meths None
|
||||||
|
in
|
||||||
|
let ty_name : ty option =
|
||||||
|
match obj with
|
||||||
|
| E_ident (n, _) -> Option.bind (find st n) (fun b -> b.b_ty)
|
||||||
|
| E_field { obj; name; _ } -> field_ty st obj name
|
||||||
|
| _ -> None
|
||||||
|
in
|
||||||
|
match ty_name with
|
||||||
|
| Some (T_named { modl; name = tn; _ }) -> (
|
||||||
|
let key = match modl with Some a -> a ^ "." ^ tn | None -> tn in
|
||||||
|
match Hashtbl.find_opt st.meths key with
|
||||||
|
| Some methods -> List.assoc_opt name methods
|
||||||
|
| None -> by_name ())
|
||||||
|
| _ -> by_name ()
|
||||||
|
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
(* 선언 *)
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
let check_fn st (d : fn_decl) =
|
||||||
|
match d.fn_body with
|
||||||
|
| None -> ()
|
||||||
|
| Some body ->
|
||||||
|
st.scopes <- [];
|
||||||
|
Hashtbl.reset st.moved;
|
||||||
|
st.depth <- 0;
|
||||||
|
st.frames <- [];
|
||||||
|
push st;
|
||||||
|
List.iter
|
||||||
|
(fun p ->
|
||||||
|
let affine = ty_affine st p.p_ty in
|
||||||
|
(* 무표기 = 빌림. own만이 소유 이전이다. *)
|
||||||
|
let use_ = affine && not p.p_own in
|
||||||
|
ignore (add st p.p_name ~affine ~use:use_ ~mut_:p.p_mut ~ty:p.p_ty ()))
|
||||||
|
d.fn_params;
|
||||||
|
let r = walk_block st (Move "반환할 수") body in
|
||||||
|
(match d.fn_ret with
|
||||||
|
| Some (T_fn { affine = false; _ }) when r.v_affine && not r.v_use ->
|
||||||
|
err st d.fn_pos
|
||||||
|
(Printf.sprintf
|
||||||
|
"%s이(가) affine 값을 capture한 클로저를 fn 타입으로 반환합니다 (affine fn이어야 합니다)"
|
||||||
|
d.fn_name)
|
||||||
|
| _ -> ());
|
||||||
|
pop st
|
||||||
|
|
||||||
|
let check ?(imports : item list = []) (m : modul) : error list =
|
||||||
|
let st =
|
||||||
|
{
|
||||||
|
aff = Hashtbl.create 16;
|
||||||
|
fields = Hashtbl.create 16;
|
||||||
|
fns = Hashtbl.create 16;
|
||||||
|
ctors = Hashtbl.create 16;
|
||||||
|
meths = Hashtbl.create 16;
|
||||||
|
scopes = [];
|
||||||
|
moved = Hashtbl.create 16;
|
||||||
|
next_id = 0;
|
||||||
|
depth = 0;
|
||||||
|
frames = [];
|
||||||
|
cl_expect = None;
|
||||||
|
errors = [];
|
||||||
|
}
|
||||||
|
in
|
||||||
|
derive_affinity st (imports @ m.items);
|
||||||
|
(* 내장 열거형의 생성자. 이름만 필요하다 *)
|
||||||
|
List.iter
|
||||||
|
(fun n -> Hashtbl.replace st.ctors n ())
|
||||||
|
[ "Some"; "None"; "Ok"; "Err" ];
|
||||||
|
List.iter
|
||||||
|
(fun it ->
|
||||||
|
match it with
|
||||||
|
| I_enum { variants; _ } ->
|
||||||
|
List.iter (fun v -> Hashtbl.replace st.ctors v.v_name ()) variants
|
||||||
|
| _ -> ())
|
||||||
|
(imports @ m.items);
|
||||||
|
List.iter
|
||||||
|
(fun it ->
|
||||||
|
match it with
|
||||||
|
| I_fn { decl; _ } ->
|
||||||
|
Hashtbl.replace st.fns decl.fn_name
|
||||||
|
{ f_self = false; f_params = decl.fn_params; f_ret = decl.fn_ret }
|
||||||
|
| I_capability { name; methods; _ } ->
|
||||||
|
Hashtbl.replace st.meths name
|
||||||
|
(List.map
|
||||||
|
(fun d ->
|
||||||
|
( d.fn_name,
|
||||||
|
{
|
||||||
|
f_self = d.fn_self;
|
||||||
|
f_params = d.fn_params;
|
||||||
|
f_ret = d.fn_ret;
|
||||||
|
} ))
|
||||||
|
methods)
|
||||||
|
| _ -> ())
|
||||||
|
(imports @ m.items);
|
||||||
|
List.iter
|
||||||
|
(fun it ->
|
||||||
|
match it with
|
||||||
|
| I_fn { decl; _ } -> check_fn st decl
|
||||||
|
(* 테스트도 같은 검사를 받는다 *)
|
||||||
|
| I_test { body; _ } ->
|
||||||
|
st.scopes <- [];
|
||||||
|
push st;
|
||||||
|
ignore (walk_block st (Move "반환할 수") body);
|
||||||
|
pop st
|
||||||
|
| _ -> ())
|
||||||
|
m.items;
|
||||||
|
List.sort
|
||||||
|
(fun a b ->
|
||||||
|
compare
|
||||||
|
(a.pos.Token.line, a.pos.Token.col)
|
||||||
|
(b.pos.Token.line, b.pos.Token.col))
|
||||||
|
(List.rev st.errors)
|
||||||
+159
-37
@@ -129,8 +129,14 @@ let rec parse_ty st =
|
|||||||
| Token.Ident n ->
|
| Token.Ident n ->
|
||||||
let p = pos st in
|
let p = pos st in
|
||||||
adv st;
|
adv st;
|
||||||
|
let modl, n =
|
||||||
|
if kind st = Token.Dot then (
|
||||||
|
adv st;
|
||||||
|
(Some n, ident st "타입 이름"))
|
||||||
|
else (None, n)
|
||||||
|
in
|
||||||
let args = if kind st = Token.LBracket then parse_targs st else [] in
|
let args = if kind st = Token.LBracket then parse_targs st else [] in
|
||||||
T_named { name = n; args; pos = p }
|
T_named { modl; name = n; args; pos = p }
|
||||||
| _ -> err_expect st "타입"
|
| _ -> err_expect st "타입"
|
||||||
|
|
||||||
and parse_fn_ty st affine p =
|
and parse_fn_ty st affine p =
|
||||||
@@ -139,7 +145,8 @@ and parse_fn_ty st affine p =
|
|||||||
if kind st = Token.RParen then []
|
if kind st = Token.RParen then []
|
||||||
else
|
else
|
||||||
let rec loop acc =
|
let rec loop acc =
|
||||||
let t = parse_ty st in
|
let own = accept st Token.Kw_own in
|
||||||
|
let t = { pt_own = own; pt_ty = parse_ty st } in
|
||||||
if accept st Token.Comma then
|
if accept st Token.Comma then
|
||||||
if kind st = Token.RParen then List.rev (t :: acc) else loop (t :: acc)
|
if kind st = Token.RParen then List.rev (t :: acc) else loop (t :: acc)
|
||||||
else List.rev (t :: acc)
|
else List.rev (t :: acc)
|
||||||
@@ -190,8 +197,14 @@ let rec parse_pattern st =
|
|||||||
| Token.Kw_false ->
|
| Token.Kw_false ->
|
||||||
adv st;
|
adv st;
|
||||||
P_lit (L_bool false, p)
|
P_lit (L_bool false, p)
|
||||||
| Token.Ident n ->
|
| Token.Ident n -> (
|
||||||
adv st;
|
adv st;
|
||||||
|
let modl, n =
|
||||||
|
if kind st = Token.Dot then (
|
||||||
|
adv st;
|
||||||
|
(Some n, ident st "생성자 이름"))
|
||||||
|
else (None, n)
|
||||||
|
in
|
||||||
if kind st = Token.LParen then (
|
if kind st = Token.LParen then (
|
||||||
adv st;
|
adv st;
|
||||||
let rec loop acc =
|
let rec loop acc =
|
||||||
@@ -203,8 +216,11 @@ let rec parse_pattern st =
|
|||||||
in
|
in
|
||||||
let args = if kind st = Token.RParen then [] else loop [] in
|
let args = if kind st = Token.RParen then [] else loop [] in
|
||||||
expect_close st Token.RParen ")";
|
expect_close st Token.RParen ")";
|
||||||
P_ctor { name = n; args; pos = p })
|
P_ctor { modl; name = n; args; pos = p })
|
||||||
else P_bind (n, p)
|
else
|
||||||
|
match modl with
|
||||||
|
| Some _ -> P_ctor { modl; name = n; args = []; pos = p }
|
||||||
|
| None -> P_bind (n, p))
|
||||||
| _ -> err_expect st "패턴"
|
| _ -> err_expect st "패턴"
|
||||||
|
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
@@ -383,6 +399,12 @@ and parse_primary st =
|
|||||||
| Token.Kw_fn -> parse_closure st
|
| Token.Kw_fn -> parse_closure st
|
||||||
| Token.Kw_if -> parse_if st
|
| Token.Kw_if -> parse_if st
|
||||||
| Token.Kw_match -> parse_match st
|
| Token.Kw_match -> parse_match st
|
||||||
|
| Token.Kw_crash ->
|
||||||
|
adv st;
|
||||||
|
expect st Token.LParen "(";
|
||||||
|
let msg = with_struct_ok st (fun () -> parse_expr st) in
|
||||||
|
expect_close st Token.RParen ")";
|
||||||
|
E_crash { msg; pos = p }
|
||||||
| Token.Kw_scope ->
|
| Token.Kw_scope ->
|
||||||
adv st;
|
adv st;
|
||||||
let name = ident st "새 scope 이름" in
|
let name = ident st "새 scope 이름" in
|
||||||
@@ -428,12 +450,14 @@ and parse_closure st =
|
|||||||
if kind st = Token.RParen then []
|
if kind st = Token.RParen then []
|
||||||
else
|
else
|
||||||
let rec loop acc =
|
let rec loop acc =
|
||||||
|
let own = accept st Token.Kw_own in
|
||||||
let n = ident st "파라미터 이름" in
|
let n = ident st "파라미터 이름" in
|
||||||
let t = if accept st Token.Colon then Some (parse_ty st) else None in
|
let t = if accept st Token.Colon then Some (parse_ty st) else None in
|
||||||
|
let cp = { cp_own = own; cp_name = n; cp_ty = t } in
|
||||||
if accept st Token.Comma then
|
if accept st Token.Comma then
|
||||||
if kind st = Token.RParen then List.rev ((n, t) :: acc)
|
if kind st = Token.RParen then List.rev (cp :: acc)
|
||||||
else loop ((n, t) :: acc)
|
else loop (cp :: acc)
|
||||||
else List.rev ((n, t) :: acc)
|
else List.rev (cp :: acc)
|
||||||
in
|
in
|
||||||
loop []
|
loop []
|
||||||
in
|
in
|
||||||
@@ -549,16 +573,11 @@ and parse_stmt st =
|
|||||||
S_return { value; pos = p }
|
S_return { value; pos = p }
|
||||||
| _ ->
|
| _ ->
|
||||||
let e = parse_expr st in
|
let e = parse_expr st in
|
||||||
if accept st Token.Eq then begin
|
if accept st Token.Eq then
|
||||||
let rec is_place = function
|
(* 대입 왼쪽에 무엇이 올 수 있는지는 구문이 아니라 이름 해소가
|
||||||
| E_ident _ -> true
|
판정한다. 구문으로 가르면 ident 하나로 대입과 식이 갈리지 않아
|
||||||
| E_field { obj; _ } -> is_place obj
|
문법이 LL(1)이 아니게 된다 (grammar.ebnf의 expr_stmt). *)
|
||||||
| _ -> false
|
S_assign { place = e; value = parse_expr st; pos = p }
|
||||||
in
|
|
||||||
if not (is_place e) then err st "대입 왼쪽에는 변수나 필드만 올 수 있습니다";
|
|
||||||
let value = parse_expr st in
|
|
||||||
S_assign { place = e; value; pos = p }
|
|
||||||
end
|
|
||||||
else S_expr e
|
else S_expr e
|
||||||
|
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
@@ -585,36 +604,69 @@ let parse_gen_params st =
|
|||||||
expect_close st Token.RBracket "]";
|
expect_close st Token.RBracket "]";
|
||||||
gs
|
gs
|
||||||
|
|
||||||
let parse_params st =
|
(* params = list<param>
|
||||||
if kind st = Token.RParen then []
|
param = [ "own" ] , plain_param
|
||||||
else
|
plain_param = [ "mut" ] , ident , ":" , type
|
||||||
let rec loop acc =
|
|
||||||
let p = pos st in
|
capability 메서드에서만 첫 자리가 다르다 (좌인수분해 — own을 먼저 먹고
|
||||||
let own = accept st Token.Kw_own in
|
그 다음을 보므로 선읽기는 여전히 한 토큰이다):
|
||||||
|
|
||||||
|
cap_params = "own" , cap_own_rest | plain_param , list_rest<param>
|
||||||
|
cap_own_rest = "self" , list_rest<param> | plain_param , list_rest<param>
|
||||||
|
|
||||||
|
일반 함수에는 `own self`를 적을 자리가 문법에 없다 — 검사기가 거부하는
|
||||||
|
것이 아니라 그 문장이 존재하지 않는다.
|
||||||
|
|
||||||
|
돌려주는 bool은 수신자를 소비하는가다. self는 fn_params에 넣지 않는다 —
|
||||||
|
호출 지점에 인자로 나타나지 않으므로 파라미터가 아니다. *)
|
||||||
|
let parse_params st ~allow_self : bool * param list =
|
||||||
|
(* [ "mut" ] ident ":" type — own을 이미 먹었을 수 있으므로 밖에서 받는다 *)
|
||||||
|
let plain_param st ~own ~p =
|
||||||
let mut_ = accept st Token.Kw_mut in
|
let mut_ = accept st Token.Kw_mut in
|
||||||
let name = ident st "파라미터 이름" in
|
let name = ident st "파라미터 이름" in
|
||||||
expect st Token.Colon ":";
|
expect st Token.Colon ":";
|
||||||
let ty = parse_ty st in
|
let ty = parse_ty st in
|
||||||
let prm =
|
|
||||||
{ p_own = own; p_mut = mut_; p_name = name; p_ty = ty; p_pos = p }
|
{ p_own = own; p_mut = mut_; p_name = name; p_ty = ty; p_pos = p }
|
||||||
in
|
in
|
||||||
|
(* list_rest<param>: 후행 콤마를 허용한다 *)
|
||||||
|
let rec rest acc =
|
||||||
if accept st Token.Comma then
|
if accept st Token.Comma then
|
||||||
if kind st = Token.RParen then List.rev (prm :: acc)
|
if kind st = Token.RParen then List.rev acc
|
||||||
else loop (prm :: acc)
|
else
|
||||||
else List.rev (prm :: acc)
|
let p = pos st in
|
||||||
|
let own = accept st Token.Kw_own in
|
||||||
|
(* self는 첫 자리에만 온다. 뒤에 나오면 수신자가 아니다 *)
|
||||||
|
if kind st = Token.Kw_self then
|
||||||
|
err st "self는 capability 메서드의 첫 파라미터로만 올 수 있습니다";
|
||||||
|
rest (plain_param st ~own ~p :: acc)
|
||||||
|
else List.rev acc
|
||||||
in
|
in
|
||||||
loop []
|
if kind st = Token.RParen then (false, [])
|
||||||
|
else
|
||||||
|
let p = pos st in
|
||||||
|
if accept st Token.Kw_own then
|
||||||
|
if allow_self && accept st Token.Kw_self then (true, rest [])
|
||||||
|
else (
|
||||||
|
if kind st = Token.Kw_self then
|
||||||
|
err st "self는 capability 메서드에서만 쓸 수 있습니다";
|
||||||
|
(false, rest [ plain_param st ~own:true ~p ]))
|
||||||
|
else (
|
||||||
|
(* own 없는 self. 빌리는 것이 기본이라 적을 말이 없다 —
|
||||||
|
같은 뜻에 두 가지 적는 법을 만들지 않는다 (철학 5) *)
|
||||||
|
if kind st = Token.Kw_self then
|
||||||
|
err st "self는 own self로만 적습니다 (수신자를 빌리는 것이 기본이라 적을 말이 없습니다)";
|
||||||
|
(false, rest [ plain_param st ~own:false ~p ]))
|
||||||
|
|
||||||
(* fn_decl = "fn" ident [gen] "(" [params] ")" {NL}
|
(* fn_decl = "fn" ident [gen] "(" [params] ")" {NL}
|
||||||
[eff_result {NL}] ["->" type {NL}] [block]
|
[eff_result {NL}] ["->" type {NL}] [block]
|
||||||
{ NEWLINE } 흡수 위치는 문법에 적힌 그대로다. *)
|
{ NEWLINE } 흡수 위치는 문법에 적힌 그대로다. *)
|
||||||
let parse_fn_decl st ~allow_body =
|
let parse_fn_decl st ~allow_body ~allow_self =
|
||||||
let p = pos st in
|
let p = pos st in
|
||||||
expect st Token.Kw_fn "fn";
|
expect st Token.Kw_fn "fn";
|
||||||
let name = ident st "함수 이름" in
|
let name = ident st "함수 이름" in
|
||||||
let gen = if kind st = Token.LBracket then parse_gen_params st else [] in
|
let gen = if kind st = Token.LBracket then parse_gen_params st else [] in
|
||||||
expect st Token.LParen "(";
|
expect st Token.LParen "(";
|
||||||
let params = parse_params st in
|
let self_, params = parse_params st ~allow_self in
|
||||||
expect_close st Token.RParen ")";
|
expect_close st Token.RParen ")";
|
||||||
skip_nl st;
|
skip_nl st;
|
||||||
let eff =
|
let eff =
|
||||||
@@ -637,6 +689,7 @@ let parse_fn_decl st ~allow_body =
|
|||||||
{
|
{
|
||||||
fn_name = name;
|
fn_name = name;
|
||||||
fn_gen = gen;
|
fn_gen = gen;
|
||||||
|
fn_self = self_;
|
||||||
fn_params = params;
|
fn_params = params;
|
||||||
fn_eff = eff;
|
fn_eff = eff;
|
||||||
fn_ret = ret;
|
fn_ret = ret;
|
||||||
@@ -722,7 +775,7 @@ let parse_capability st ~pub =
|
|||||||
let rec loop acc =
|
let rec loop acc =
|
||||||
if kind st = Token.RBrace then List.rev acc
|
if kind st = Token.RBrace then List.rev acc
|
||||||
else
|
else
|
||||||
let m = parse_fn_decl st ~allow_body:false in
|
let m = parse_fn_decl st ~allow_body:false ~allow_self:true in
|
||||||
skip_nl st;
|
skip_nl st;
|
||||||
loop (m :: acc)
|
loop (m :: acc)
|
||||||
in
|
in
|
||||||
@@ -749,10 +802,20 @@ let parse_item st =
|
|||||||
adv st;
|
adv st;
|
||||||
let name = ident st "재수출할 이름" in
|
let name = ident st "재수출할 이름" in
|
||||||
I_reexport { name; pos = p }
|
I_reexport { name; pos = p }
|
||||||
|
| Token.Kw_test ->
|
||||||
|
adv st;
|
||||||
|
let name =
|
||||||
|
match kind st with
|
||||||
|
| Token.Str s ->
|
||||||
|
adv st;
|
||||||
|
s
|
||||||
|
| _ -> err_expect st "테스트 이름 (문자열)"
|
||||||
|
in
|
||||||
|
I_test { name; body = parse_block st; pos = p }
|
||||||
| _ -> (
|
| _ -> (
|
||||||
let pub = accept st Token.Kw_pub in
|
let pub = accept st Token.Kw_pub in
|
||||||
match kind st with
|
match kind st with
|
||||||
| Token.Kw_fn -> I_fn { pub; decl = parse_fn_decl st ~allow_body:true }
|
| Token.Kw_fn -> I_fn { pub; decl = parse_fn_decl st ~allow_body:true ~allow_self:false }
|
||||||
| Token.Kw_copyable ->
|
| Token.Kw_copyable ->
|
||||||
adv st;
|
adv st;
|
||||||
parse_struct st ~pub ~copyable:true
|
parse_struct st ~pub ~copyable:true
|
||||||
@@ -769,20 +832,79 @@ let parse_item st =
|
|||||||
I_const { pub; name; ty; value; pos = p }
|
I_const { pub; name; ty; value; pos = p }
|
||||||
| _ -> err_expect st "선언 (fn, struct, enum, capability, const)")
|
| _ -> err_expect st "선언 (fn, struct, enum, capability, const)")
|
||||||
|
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
(* 오류 복구 *)
|
||||||
|
(* *)
|
||||||
|
(* 항목 단위로만 회복한다. 오류가 난 선언은 통째로 버리고 다음 선언에서 *)
|
||||||
|
(* 다시 시작한다 — 문 단위로 더 잘게 회복하려 하면 파서가 추측을 하게 되고, *)
|
||||||
|
(* 틀린 추측은 없는 오류를 지어낸다. 한 항목에 오류 하나가 상한이라는 것은 *)
|
||||||
|
(* 정직한 한계이지 숨길 것이 아니다. *)
|
||||||
|
(* *)
|
||||||
|
(* 동기화 지점: 중괄호 깊이 0이고, 줄 첫머리이며, 선언을 시작할 수 있는 토큰. *)
|
||||||
|
(* 세 조건이 다 필요하다. 본문 안의 fn을 새 항목으로 오인하면 그 뒤가 전부 *)
|
||||||
|
(* 어긋난다. *)
|
||||||
|
|
||||||
|
let item_starts =
|
||||||
|
[
|
||||||
|
Token.Kw_import;
|
||||||
|
Token.Kw_reexport;
|
||||||
|
Token.Kw_pub;
|
||||||
|
Token.Kw_fn;
|
||||||
|
Token.Kw_struct;
|
||||||
|
Token.Kw_enum;
|
||||||
|
Token.Kw_capability;
|
||||||
|
Token.Kw_const;
|
||||||
|
Token.Kw_copyable;
|
||||||
|
]
|
||||||
|
|
||||||
|
let at_line_start st = st.i > 0 && st.toks.(st.i - 1).Token.kind = Token.Newline
|
||||||
|
|
||||||
|
let sync st =
|
||||||
|
let depth = ref 0 in
|
||||||
|
let fin = ref false in
|
||||||
|
while not !fin do
|
||||||
|
match kind st with
|
||||||
|
| Token.Eof -> fin := true
|
||||||
|
| Token.LBrace ->
|
||||||
|
incr depth;
|
||||||
|
adv st
|
||||||
|
| Token.RBrace ->
|
||||||
|
decr depth;
|
||||||
|
adv st
|
||||||
|
| k ->
|
||||||
|
if !depth <= 0 && at_line_start st && List.mem k item_starts then
|
||||||
|
fin := true
|
||||||
|
else adv st
|
||||||
|
done
|
||||||
|
|
||||||
let parse_module st =
|
let parse_module st =
|
||||||
skip_nl st;
|
skip_nl st;
|
||||||
|
let errors = ref [] in
|
||||||
let rec loop acc =
|
let rec loop acc =
|
||||||
if kind st = Token.Eof then List.rev acc
|
if kind st = Token.Eof then List.rev acc
|
||||||
else
|
else
|
||||||
let it = parse_item st in
|
match parse_item st with
|
||||||
|
| it ->
|
||||||
skip_nl st;
|
skip_nl st;
|
||||||
loop (it :: acc)
|
loop (it :: acc)
|
||||||
|
| exception Error e ->
|
||||||
|
errors := e :: !errors;
|
||||||
|
let before = st.i in
|
||||||
|
sync st;
|
||||||
|
(* 진행 보장. 같은 자리에서 다시 실패하면 무한 루프다. *)
|
||||||
|
if st.i = before then adv st;
|
||||||
|
skip_nl st;
|
||||||
|
loop acc
|
||||||
in
|
in
|
||||||
{ items = loop [] }
|
let items = loop [] in
|
||||||
|
({ items }, List.rev !errors)
|
||||||
|
|
||||||
let parse (tokens : Token.t list) : modul =
|
let parse_all (tokens : Token.t list) : modul * error list =
|
||||||
let st = { toks = Array.of_list tokens; i = 0; no_struct = false } in
|
let st = { toks = Array.of_list tokens; i = 0; no_struct = false } in
|
||||||
parse_module st
|
parse_module st
|
||||||
|
|
||||||
|
let parse (tokens : Token.t list) : modul =
|
||||||
|
match parse_all tokens with m, [] -> m | _, e :: _ -> raise (Error e)
|
||||||
|
|
||||||
let parse_result tokens =
|
let parse_result tokens =
|
||||||
match parse tokens with m -> Ok m | exception Error e -> Error e
|
match parse_all tokens with m, [] -> Ok m | _, e :: _ -> Error e
|
||||||
|
|||||||
@@ -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)
|
||||||
+69
-12
@@ -11,7 +11,10 @@
|
|||||||
|
|
||||||
open Ast
|
open Ast
|
||||||
|
|
||||||
type error = { pos : Token.pos; msg : string }
|
(* blocking: 이름을 해소하지 못했다는 뜻이고, 이후 단계는 의미가 없다.
|
||||||
|
lint는 다르다 — 코드는 분석 가능하고 검사도 계속되어야 한다.
|
||||||
|
둘을 같은 통에 넣으면 lint 하나가 진짜 타입 오류를 가린다. *)
|
||||||
|
type error = { pos : Token.pos; msg : string; blocking : bool }
|
||||||
type info = { externals : (string * Token.pos) list }
|
type info = { externals : (string * Token.pos) list }
|
||||||
type item_kind = K_fn | K_type | K_const | K_import
|
type item_kind = K_fn | K_type | K_const | K_import
|
||||||
|
|
||||||
@@ -23,13 +26,20 @@ type state = {
|
|||||||
mutable scopes : (string * bool) list list; (* 이름 -> 가변 여부 *)
|
mutable scopes : (string * bool) list list; (* 이름 -> 가변 여부 *)
|
||||||
mutable errors : error list;
|
mutable errors : error list;
|
||||||
mutable ext : (string * Token.pos) list;
|
mutable ext : (string * Token.pos) list;
|
||||||
|
(* 실제로 참조된 import 별칭 *)
|
||||||
|
used : (string, unit) Hashtbl.t;
|
||||||
}
|
}
|
||||||
|
|
||||||
let builtin_types =
|
let builtin_types =
|
||||||
[ "Int"; "Bool"; "String"; "Unit"; "List"; "Option"; "Result" ]
|
[ "Int"; "Bool"; "String"; "Unit"; "List"; "Option"; "Result" ]
|
||||||
|
|
||||||
let builtin_values = [ "unit"; "Ok"; "Err"; "Some"; "None" ]
|
let builtin_values = [ "unit"; "Ok"; "Err"; "Some"; "None" ]
|
||||||
let error st pos msg = st.errors <- { pos; msg } :: st.errors
|
let error st pos msg = st.errors <- { pos; msg; blocking = true } :: st.errors
|
||||||
|
let lint st pos msg = st.errors <- { pos; msg; blocking = false } :: st.errors
|
||||||
|
|
||||||
|
let use_alias st a =
|
||||||
|
if Hashtbl.find_opt st.items a = Some K_import then
|
||||||
|
Hashtbl.replace st.used a ()
|
||||||
|
|
||||||
let external_ref st name pos =
|
let external_ref st name pos =
|
||||||
if not (List.mem_assoc name st.ext) then st.ext <- (name, pos) :: st.ext
|
if not (List.mem_assoc name st.ext) then st.ext <- (name, pos) :: st.ext
|
||||||
@@ -77,7 +87,13 @@ let resolve_eff_atom st = function
|
|||||||
names
|
names
|
||||||
|
|
||||||
let rec resolve_ty st = function
|
let rec resolve_ty st = function
|
||||||
| T_named { name; args; pos } ->
|
(* 한정된 이름은 별칭이 이 모듈에 있는지만 본다. 그 모듈 안에 그 타입이
|
||||||
|
있는지는 모듈 하나만 보고 결정할 수 없다 — 외부 참조로 기록한다. *)
|
||||||
|
| T_named { modl = Some a; args; pos; _ } ->
|
||||||
|
use_alias st a;
|
||||||
|
if Hashtbl.find_opt st.items a <> Some K_import then external_ref st a pos;
|
||||||
|
List.iter (resolve_targ st pos) args
|
||||||
|
| T_named { modl = None; name; args; pos } ->
|
||||||
if
|
if
|
||||||
(not (List.mem name st.ty_params))
|
(not (List.mem name st.ty_params))
|
||||||
&& (not (List.mem name builtin_types))
|
&& (not (List.mem name builtin_types))
|
||||||
@@ -88,7 +104,7 @@ let rec resolve_ty st = function
|
|||||||
then external_ref st name pos;
|
then external_ref st name pos;
|
||||||
List.iter (resolve_targ st pos) args
|
List.iter (resolve_targ st pos) args
|
||||||
| T_fn { params; eff; ret; pos; _ } ->
|
| T_fn { params; eff; ret; pos; _ } ->
|
||||||
List.iter (resolve_ty st) params;
|
List.iter (fun (p : fn_param_ty) -> resolve_ty st p.pt_ty) params;
|
||||||
(match eff with None -> () | Some a -> resolve_eff_atom st (a, pos));
|
(match eff with None -> () | Some a -> resolve_eff_atom st (a, pos));
|
||||||
Option.iter (resolve_ty st) ret
|
Option.iter (resolve_ty st) ret
|
||||||
|
|
||||||
@@ -118,7 +134,11 @@ let rec resolve_pattern st seen = function
|
|||||||
else (
|
else (
|
||||||
seen := n :: !seen;
|
seen := n :: !seen;
|
||||||
bind st pos n false))
|
bind st pos n false))
|
||||||
| P_ctor { name; args; pos } ->
|
| P_ctor { modl = Some a; args; pos; _ } ->
|
||||||
|
use_alias st a;
|
||||||
|
if Hashtbl.find_opt st.items a <> Some K_import then external_ref st a pos;
|
||||||
|
List.iter (resolve_pattern st seen) args
|
||||||
|
| P_ctor { modl = None; name; args; pos } ->
|
||||||
(match Hashtbl.find_opt st.ctors name with
|
(match Hashtbl.find_opt st.ctors name with
|
||||||
| Some (enum, arity) when arity <> List.length args ->
|
| Some (enum, arity) when arity <> List.length args ->
|
||||||
error st pos
|
error st pos
|
||||||
@@ -134,6 +154,7 @@ let rec resolve_pattern st seen = function
|
|||||||
|
|
||||||
let rec resolve_expr st = function
|
let rec resolve_expr st = function
|
||||||
| E_lit _ -> ()
|
| E_lit _ -> ()
|
||||||
|
| E_crash { msg; _ } -> resolve_expr st msg
|
||||||
| E_ident (n, pos) ->
|
| E_ident (n, pos) ->
|
||||||
if lookup_local st n = None then
|
if lookup_local st n = None then
|
||||||
if Hashtbl.mem st.items n then ()
|
if Hashtbl.mem st.items n then ()
|
||||||
@@ -144,14 +165,14 @@ let rec resolve_expr st = function
|
|||||||
else external_ref st n pos
|
else external_ref st n pos
|
||||||
| E_list (xs, _) -> List.iter (resolve_expr st) xs
|
| E_list (xs, _) -> List.iter (resolve_expr st) xs
|
||||||
| E_struct { name; fields; pos } ->
|
| E_struct { name; fields; pos } ->
|
||||||
resolve_ty st (T_named { name; args = []; pos });
|
resolve_ty st (T_named { modl = None; name; args = []; pos });
|
||||||
List.iter (fun (_, e) -> resolve_expr st e) fields
|
List.iter (fun (_, e) -> resolve_expr st e) fields
|
||||||
| E_closure c ->
|
| E_closure c ->
|
||||||
push st;
|
push st;
|
||||||
List.iter
|
List.iter
|
||||||
(fun (n, t) ->
|
(fun (p : cl_param) ->
|
||||||
Option.iter (resolve_ty st) t;
|
Option.iter (resolve_ty st) p.cp_ty;
|
||||||
bind st c.cl_pos n false)
|
bind st c.cl_pos p.cp_name false)
|
||||||
c.cl_params;
|
c.cl_params;
|
||||||
(match c.cl_eff with
|
(match c.cl_eff with
|
||||||
| None -> ()
|
| None -> ()
|
||||||
@@ -191,7 +212,26 @@ let rec resolve_expr st = function
|
|||||||
| E_call { callee; args; _ } ->
|
| E_call { callee; args; _ } ->
|
||||||
resolve_expr st callee;
|
resolve_expr st callee;
|
||||||
List.iter (resolve_expr st) args
|
List.iter (resolve_expr st) args
|
||||||
| E_field { obj; _ } -> resolve_expr st obj
|
| E_field { obj; pos; _ } ->
|
||||||
|
(* Alias.f — 별칭 접근도 사용이다.
|
||||||
|
|
||||||
|
왼쪽이 지역 값도, 가져온 모듈도, 이 모듈의 선언도 아니면 그 이름은
|
||||||
|
그냥 없는 것이다. 바깥 참조로 넘기면 안 된다 — 넘기는 순간 오른쪽이
|
||||||
|
전부 불투명해지고 그 아래 검사가 통째로 공허해진다 (D1과 같은 병).
|
||||||
|
|
||||||
|
내장 타입 이름도 모듈이 아니다. 타입으로 쓰는 데는 import가 필요
|
||||||
|
없지만 그 타입에 딸린 함수를 쓰려면 가져와야 한다 — prelude가
|
||||||
|
없다는 규칙에 예외를 두지 않는다. *)
|
||||||
|
(match obj with
|
||||||
|
| E_ident (o, _) when lookup_local st o = None ->
|
||||||
|
if Hashtbl.mem st.items o then use_alias st o
|
||||||
|
else if List.mem o builtin_types then
|
||||||
|
error st pos
|
||||||
|
(Printf.sprintf "%s은(는) 내장 타입이지 모듈이 아닙니다 (딸린 함수를 쓰려면 가져오십시오)" o)
|
||||||
|
else if not (Hashtbl.mem st.ctors o || List.mem o builtin_values) then
|
||||||
|
error st pos (Printf.sprintf "%s은(는) 가져온 모듈도 지역 값도 아닙니다" o)
|
||||||
|
| _ -> ());
|
||||||
|
resolve_expr st obj
|
||||||
| E_inst { callee; args; pos } ->
|
| E_inst { callee; args; pos } ->
|
||||||
resolve_expr st callee;
|
resolve_expr st callee;
|
||||||
List.iter (resolve_targ st pos) args
|
List.iter (resolve_targ st pos) args
|
||||||
@@ -236,7 +276,7 @@ and resolve_stmt st = function
|
|||||||
| _ -> None
|
| _ -> None
|
||||||
in
|
in
|
||||||
match root place with
|
match root place with
|
||||||
| None -> ()
|
| None -> error st pos "대입 왼쪽에는 변수나 필드만 올 수 있습니다"
|
||||||
| Some n -> (
|
| Some n -> (
|
||||||
match lookup_local st n with
|
match lookup_local st n with
|
||||||
| Some true -> ()
|
| Some true -> ()
|
||||||
@@ -304,6 +344,7 @@ let resolve (m : modul) : info * error list =
|
|||||||
scopes = [];
|
scopes = [];
|
||||||
errors = [];
|
errors = [];
|
||||||
ext = [];
|
ext = [];
|
||||||
|
used = Hashtbl.create 8;
|
||||||
}
|
}
|
||||||
in
|
in
|
||||||
(* 1차: 모듈 수준 이름을 모은다. 선언 순서에 의존하지 않는다. *)
|
(* 1차: 모듈 수준 이름을 모은다. 선언 순서에 의존하지 않는다. *)
|
||||||
@@ -324,7 +365,7 @@ let resolve (m : modul) : info * error list =
|
|||||||
variants
|
variants
|
||||||
| I_capability { name; pos; _ } -> declare st pos name K_type
|
| I_capability { name; pos; _ } -> declare st pos name K_type
|
||||||
| I_const { name; pos; _ } -> declare st pos name K_const
|
| I_const { name; pos; _ } -> declare st pos name K_const
|
||||||
| I_reexport _ -> ())
|
| I_test _ | I_reexport _ -> ())
|
||||||
m.items;
|
m.items;
|
||||||
(* 2차: 본문을 훑는다. *)
|
(* 2차: 본문을 훑는다. *)
|
||||||
List.iter
|
List.iter
|
||||||
@@ -352,8 +393,24 @@ let resolve (m : modul) : info * error list =
|
|||||||
resolve_ty st ty;
|
resolve_ty st ty;
|
||||||
push st;
|
push st;
|
||||||
resolve_expr st value;
|
resolve_expr st value;
|
||||||
|
pop st
|
||||||
|
(* 테스트는 파라미터 없는 본문이다. 나머지는 함수와 같다 *)
|
||||||
|
| I_test { body; _ } ->
|
||||||
|
push st;
|
||||||
|
resolve_block st body;
|
||||||
pop st)
|
pop st)
|
||||||
m.items;
|
m.items;
|
||||||
|
(* 미사용 import는 오류다. 취향 문제가 아니라 invalidation 표면 문제다 —
|
||||||
|
쓰지도 않는 모듈의 시그니처가 바뀌면 이 모듈이 재검사된다. 증분 루프의
|
||||||
|
비용을 아무 이유 없이 넓히는 선언은 남겨둘 수 없다. *)
|
||||||
|
List.iter
|
||||||
|
(fun it ->
|
||||||
|
match it with
|
||||||
|
| I_import { alias; pos; _ } when not (Hashtbl.mem st.used alias) ->
|
||||||
|
lint st pos
|
||||||
|
(Printf.sprintf "%s을(를) 가져왔지만 쓰지 않습니다 (재검사 범위만 넓힙니다)" alias)
|
||||||
|
| _ -> ())
|
||||||
|
m.items;
|
||||||
let by_pos (_, a) (_, b) =
|
let by_pos (_, a) (_, b) =
|
||||||
compare (a.Token.line, a.Token.col) (b.Token.line, b.Token.col)
|
compare (a.Token.line, a.Token.col) (b.Token.line, b.Token.col)
|
||||||
in
|
in
|
||||||
|
|||||||
+304
@@ -0,0 +1,304 @@
|
|||||||
|
(* 모듈 로딩, interface 캐시, 그리고 고정점 invalidation.
|
||||||
|
|
||||||
|
여기가 v0가 존재하는 이유다. 검사 자체보다 "무엇을 다시 검사해야 하는가"를
|
||||||
|
좁게 유지하는 것이 아키텍처의 주장이고, 그 주장은 측정으로만 증명된다.
|
||||||
|
|
||||||
|
전파는 고정점 규칙이다 (문서 P8):
|
||||||
|
1. 변경된 모듈 자체를 재검사
|
||||||
|
2. 재검사 전후의 interface hash를 비교
|
||||||
|
3. 달라졌을 때만 그 모듈의 dependents를 큐에 추가
|
||||||
|
4. 큐가 빌 때까지 반복
|
||||||
|
순서가 중요하다. dependents를 먼저 재검사하면 "본문만 수정 시 downstream
|
||||||
|
0건"이 성립하지 않는다 — hash 비교가 dependents 재검사보다 앞서야 한다. *)
|
||||||
|
|
||||||
|
type error = { file : string; line : int; col : int; message : string }
|
||||||
|
|
||||||
|
let string_of_error { file; line; col; message } =
|
||||||
|
Printf.sprintf "%s:%d:%d: %s" file line col message
|
||||||
|
|
||||||
|
type entry = {
|
||||||
|
path : string;
|
||||||
|
ast : Ast.modul;
|
||||||
|
imports : (string * string) list; (* 별칭 -> 해소된 경로 *)
|
||||||
|
iface : Iface.t;
|
||||||
|
errors : error list;
|
||||||
|
}
|
||||||
|
|
||||||
|
type t = {
|
||||||
|
root : string;
|
||||||
|
std : string option; (* 표준 라이브러리 디렉터리 *)
|
||||||
|
modules : (string, entry) Hashtbl.t;
|
||||||
|
(* 통계: 무엇이 몇 번 재검사됐는지. 측정이 목적이므로 처음부터 센다. *)
|
||||||
|
mutable checked : string list;
|
||||||
|
}
|
||||||
|
|
||||||
|
(* 표준 라이브러리를 찾는다. COOL_STD가 있으면 그것, 없으면 위로 올라가며
|
||||||
|
std/list.cool을 찾는다. 못 찾으면 없는 것이고, 그때 std 이름들은 외부
|
||||||
|
참조로 남는다 — 없다고 말하지 않는다. *)
|
||||||
|
let find_std root =
|
||||||
|
match Sys.getenv_opt "COOL_STD" with
|
||||||
|
| Some d when Sys.file_exists d -> Some d
|
||||||
|
| _ ->
|
||||||
|
let rec up dir n =
|
||||||
|
if n = 0 then None
|
||||||
|
else
|
||||||
|
let cand = Filename.concat dir "std" in
|
||||||
|
if Sys.file_exists (Filename.concat cand "list.cool") then Some cand
|
||||||
|
else
|
||||||
|
let parent = Filename.dirname dir in
|
||||||
|
if parent = dir then None else up parent (n - 1)
|
||||||
|
in
|
||||||
|
up
|
||||||
|
(if Filename.is_relative root then Filename.concat (Sys.getcwd ()) root
|
||||||
|
else root)
|
||||||
|
8
|
||||||
|
|
||||||
|
let create ?(root = ".") ?std () =
|
||||||
|
{
|
||||||
|
root;
|
||||||
|
std = (match std with Some _ -> std | None -> find_std root);
|
||||||
|
modules = Hashtbl.create 16;
|
||||||
|
checked = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
(* 패키지 경로와 지역 모듈 경로를 구분한다. 첫 세그먼트에 점이 있으면
|
||||||
|
패키지 참조다 (cool.dev/std/list). v0에는 패키지 해소가 없으므로 그런
|
||||||
|
import는 불투명하게 남는다 — 없다고 말하지 않는다. *)
|
||||||
|
let is_package path =
|
||||||
|
(* ./ 와 ../ 로 시작하면 상대 경로다. 첫 세그먼트에 점이 있다는 것만 보면
|
||||||
|
".."이 패키지로 오인된다 — 그러면 import가 조용히 해소되지 않고, 그
|
||||||
|
모듈의 이름이 전부 불투명해져 검사가 통째로 공허해진다. *)
|
||||||
|
let starts p =
|
||||||
|
String.length path >= String.length p
|
||||||
|
&& String.sub path 0 (String.length p) = p
|
||||||
|
in
|
||||||
|
if starts "./" || starts "../" then false
|
||||||
|
else
|
||||||
|
match String.index_opt path '/' with
|
||||||
|
| Some i -> String.contains (String.sub path 0 i) '.'
|
||||||
|
| None -> String.contains path '.'
|
||||||
|
|
||||||
|
let std_prefix = "cool.dev/std/"
|
||||||
|
|
||||||
|
let starts_with p s =
|
||||||
|
String.length s >= String.length p && String.sub s 0 (String.length p) = p
|
||||||
|
|
||||||
|
(* 경로를 파일로 바꾼다. 바꿀 수 없으면 None — v0에는 패키지 해소가 없으므로
|
||||||
|
표준 라이브러리 밖의 패키지는 불투명하게 남는다. *)
|
||||||
|
let resolve_import st path : string option =
|
||||||
|
if starts_with std_prefix path then
|
||||||
|
let name =
|
||||||
|
String.sub path (String.length std_prefix)
|
||||||
|
(String.length path - String.length std_prefix)
|
||||||
|
in
|
||||||
|
match st.std with
|
||||||
|
| Some d -> Some (Filename.concat d (name ^ ".cool"))
|
||||||
|
| None -> None
|
||||||
|
else if is_package path then None
|
||||||
|
else Some (Filename.concat st.root (path ^ ".cool"))
|
||||||
|
|
||||||
|
let read_file file =
|
||||||
|
let ic = open_in_bin file in
|
||||||
|
let n = in_channel_length ic in
|
||||||
|
let s = really_input_string ic n in
|
||||||
|
close_in ic;
|
||||||
|
s
|
||||||
|
|
||||||
|
let err_of file (pos : Token.pos) msg =
|
||||||
|
{ file; line = pos.line; col = pos.col; message = msg }
|
||||||
|
|
||||||
|
(* 파서는 항목 단위로 회복하므로 오류가 여럿일 수 있다. 첫 오류에서 멈추면
|
||||||
|
고칠 때마다 다시 돌려야 하고, 그것이 빠른 루프의 반대다. *)
|
||||||
|
let parse_file file =
|
||||||
|
match Lexer.lex_result (read_file file) with
|
||||||
|
| Error e -> Error [ err_of file e.pos e.msg ]
|
||||||
|
| Ok toks -> (
|
||||||
|
match Parser.parse_all toks with
|
||||||
|
| m, [] -> Ok m
|
||||||
|
| _, errs ->
|
||||||
|
Error
|
||||||
|
(List.map (fun (e : Parser.error) -> err_of file e.pos e.msg) errs))
|
||||||
|
|
||||||
|
let imports_of (m : Ast.modul) =
|
||||||
|
List.filter_map
|
||||||
|
(function
|
||||||
|
| Ast.I_import { path; alias; _ } -> Some (alias, path) | _ -> None)
|
||||||
|
m.items
|
||||||
|
|
||||||
|
(* 한 모듈을 검사한다. 의존 모듈의 interface는 이미 로드되어 있어야 한다. *)
|
||||||
|
let check_module st path : entry =
|
||||||
|
st.checked <- path :: st.checked;
|
||||||
|
match parse_file path with
|
||||||
|
| Error es ->
|
||||||
|
{
|
||||||
|
path;
|
||||||
|
ast = { items = [] };
|
||||||
|
imports = [];
|
||||||
|
iface = { items = []; hash = "" };
|
||||||
|
errors = es;
|
||||||
|
}
|
||||||
|
| Ok ast ->
|
||||||
|
let imports =
|
||||||
|
List.filter_map
|
||||||
|
(fun (a, p) ->
|
||||||
|
match resolve_import st p with
|
||||||
|
| Some f -> Some (a, f)
|
||||||
|
| None -> None)
|
||||||
|
(imports_of ast)
|
||||||
|
in
|
||||||
|
(* 의존 모듈의 exported surface를 소비 측 별칭으로 한정해 합친다.
|
||||||
|
여기서부터 검사기는 "이 모듈 + 아는 외부 표면"만 본다. *)
|
||||||
|
let dep_surface =
|
||||||
|
List.concat_map
|
||||||
|
(fun (alias, p) ->
|
||||||
|
match Hashtbl.find_opt st.modules p with
|
||||||
|
| Some e -> Iface.qualify alias e.iface.Iface.items
|
||||||
|
| None -> [])
|
||||||
|
imports
|
||||||
|
in
|
||||||
|
let iface = Iface.of_module ast in
|
||||||
|
let _, rerrors = Resolve.resolve ast in
|
||||||
|
(* 이름을 해소하지 못했으면 뒤 단계는 의미가 없다. lint는 막지 않는다. *)
|
||||||
|
let blocking =
|
||||||
|
List.filter (fun (e : Resolve.error) -> e.blocking) rerrors
|
||||||
|
in
|
||||||
|
let errors =
|
||||||
|
List.map (fun (e : Resolve.error) -> err_of path e.pos e.msg) rerrors
|
||||||
|
in
|
||||||
|
let errors =
|
||||||
|
if blocking <> [] then errors
|
||||||
|
else
|
||||||
|
errors
|
||||||
|
@ List.map
|
||||||
|
(fun (e : Typecheck.error) -> err_of path e.pos e.msg)
|
||||||
|
(Typecheck.check ~imports:dep_surface ast)
|
||||||
|
@ List.map
|
||||||
|
(fun (e : Move.error) -> err_of path e.pos e.msg)
|
||||||
|
(Move.check ~imports:dep_surface ast)
|
||||||
|
in
|
||||||
|
let errors =
|
||||||
|
List.sort (fun a b -> compare (a.line, a.col) (b.line, b.col)) errors
|
||||||
|
in
|
||||||
|
{ path; ast; imports; iface; errors }
|
||||||
|
|
||||||
|
(* 의존 순서대로 로드한다. 순환은 오류다. *)
|
||||||
|
let rec load st ?(visiting = []) path : unit =
|
||||||
|
if Hashtbl.mem st.modules path then ()
|
||||||
|
else if List.mem path visiting then ()
|
||||||
|
else if not (Sys.file_exists path) then
|
||||||
|
Hashtbl.replace st.modules path
|
||||||
|
{
|
||||||
|
path;
|
||||||
|
ast = { items = [] };
|
||||||
|
imports = [];
|
||||||
|
iface = { items = []; hash = "" };
|
||||||
|
errors =
|
||||||
|
[ { file = path; line = 0; col = 0; message = "모듈을 찾을 수 없습니다" } ];
|
||||||
|
}
|
||||||
|
else begin
|
||||||
|
(match parse_file path with
|
||||||
|
| Error _ -> ()
|
||||||
|
| Ok ast ->
|
||||||
|
List.iter
|
||||||
|
(fun (_, p) ->
|
||||||
|
match resolve_import st p with
|
||||||
|
| Some f -> load st ~visiting:(path :: visiting) f
|
||||||
|
| None -> ())
|
||||||
|
(imports_of ast));
|
||||||
|
Hashtbl.replace st.modules path (check_module st path)
|
||||||
|
end
|
||||||
|
|
||||||
|
let dependents st path =
|
||||||
|
Hashtbl.fold
|
||||||
|
(fun p e acc ->
|
||||||
|
if List.exists (fun (_, d) -> d = path) e.imports then p :: acc else acc)
|
||||||
|
st.modules []
|
||||||
|
|
||||||
|
(* 고정점 전파. 반환값은 실제로 재검사한 모듈 목록이다. *)
|
||||||
|
let recheck st (changed : string list) : string list =
|
||||||
|
st.checked <- [];
|
||||||
|
let queue = ref changed in
|
||||||
|
let seen = Hashtbl.create 8 in
|
||||||
|
while !queue <> [] do
|
||||||
|
let path = List.hd !queue in
|
||||||
|
queue := List.tl !queue;
|
||||||
|
if not (Hashtbl.mem seen path) then begin
|
||||||
|
Hashtbl.replace seen path ();
|
||||||
|
let before =
|
||||||
|
match Hashtbl.find_opt st.modules path with
|
||||||
|
| Some e -> e.iface.Iface.hash
|
||||||
|
| None -> ""
|
||||||
|
in
|
||||||
|
let entry = check_module st path in
|
||||||
|
Hashtbl.replace st.modules path entry;
|
||||||
|
(* hash 비교가 dependents 재검사보다 앞선다 *)
|
||||||
|
if entry.iface.Iface.hash <> before then
|
||||||
|
queue := !queue @ dependents st path
|
||||||
|
end
|
||||||
|
done;
|
||||||
|
List.rev st.checked
|
||||||
|
|
||||||
|
let errors st =
|
||||||
|
Hashtbl.fold (fun _ e acc -> e.errors @ acc) st.modules []
|
||||||
|
|> List.sort (fun a b ->
|
||||||
|
compare (a.file, a.line, a.col) (b.file, b.line, b.col))
|
||||||
|
|
||||||
|
let find st path = Hashtbl.find_opt st.modules path
|
||||||
|
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
(* 실행 *)
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
(* main이 선언한 파라미터의 타입 이름을 뽑는다. 런타임은 이 목록만 보고
|
||||||
|
권한을 만든다 — 선언하지 않은 capability는 프로그램에 존재하지 않는다. *)
|
||||||
|
let main_params (m : Ast.modul) =
|
||||||
|
let rec ty_name (t : Ast.ty) =
|
||||||
|
match t with
|
||||||
|
| Ast.T_named { modl; name; _ } -> (
|
||||||
|
match modl with Some a -> a ^ "." ^ name | None -> name)
|
||||||
|
| Ast.T_fn _ -> "<fn>"
|
||||||
|
in
|
||||||
|
List.concat_map
|
||||||
|
(function
|
||||||
|
| Ast.I_fn { decl; _ } when decl.fn_name = "main" ->
|
||||||
|
List.map
|
||||||
|
(fun (p : Ast.param) -> (p.p_name, ty_name p.p_ty))
|
||||||
|
decl.fn_params
|
||||||
|
| _ -> [])
|
||||||
|
m.items
|
||||||
|
|
||||||
|
(* 출력과 실패를 함께 돌려준다. 실패해도 그때까지 나온 것은 보여줘야 한다 *)
|
||||||
|
let run ?(args = []) st path : string * error option =
|
||||||
|
load st path;
|
||||||
|
let errs = errors st in
|
||||||
|
if errs <> [] then ("", Some (List.hd errs))
|
||||||
|
else
|
||||||
|
match find st path with
|
||||||
|
| None ->
|
||||||
|
("", Some { file = path; line = 0; col = 0; message = "모듈을 찾을 수 없습니다" })
|
||||||
|
| Some e -> (
|
||||||
|
(* IR은 그래프 전체를 받는다. 별칭이 실행 의미에 남지 않도록. *)
|
||||||
|
let mods =
|
||||||
|
Hashtbl.fold
|
||||||
|
(fun p (d : entry) acc ->
|
||||||
|
{ Ir.m_path = p; m_ast = d.ast; m_deps = d.imports } :: acc)
|
||||||
|
st.modules []
|
||||||
|
in
|
||||||
|
let prog = Ir.of_program mods in
|
||||||
|
match Interp.run ~args prog path (main_params e.ast) with
|
||||||
|
| out, None -> (out, None)
|
||||||
|
| out, Some (pos, msg) -> (out, Some (err_of path pos msg)))
|
||||||
|
|
||||||
|
(* 모듈 그래프를 로드하고 그 안의 테스트를 전부 돌린다 *)
|
||||||
|
let test ?(filter = "") st path : (Interp.test_result list, error list) result =
|
||||||
|
load st path;
|
||||||
|
let errs = errors st in
|
||||||
|
if errs <> [] then Error errs
|
||||||
|
else
|
||||||
|
let mods =
|
||||||
|
Hashtbl.fold
|
||||||
|
(fun p (d : entry) acc ->
|
||||||
|
{ Ir.m_path = p; m_ast = d.ast; m_deps = d.imports } :: acc)
|
||||||
|
st.modules []
|
||||||
|
in
|
||||||
|
Ok (Interp.run_tests ~filter (Ir.of_program mods))
|
||||||
@@ -20,6 +20,7 @@ type kind =
|
|||||||
| Kw_let
|
| Kw_let
|
||||||
| Kw_mut
|
| Kw_mut
|
||||||
| Kw_own
|
| Kw_own
|
||||||
|
| Kw_self
|
||||||
| Kw_affine
|
| Kw_affine
|
||||||
| Kw_copyable
|
| Kw_copyable
|
||||||
| Kw_effects
|
| Kw_effects
|
||||||
@@ -28,6 +29,8 @@ type kind =
|
|||||||
| Kw_else
|
| Kw_else
|
||||||
| Kw_match
|
| Kw_match
|
||||||
| Kw_scope
|
| Kw_scope
|
||||||
|
| Kw_crash
|
||||||
|
| Kw_test
|
||||||
| Kw_true
|
| Kw_true
|
||||||
| Kw_false
|
| Kw_false
|
||||||
(* 구두점 *)
|
(* 구두점 *)
|
||||||
@@ -79,6 +82,7 @@ let keyword = function
|
|||||||
| "let" -> Some Kw_let
|
| "let" -> Some Kw_let
|
||||||
| "mut" -> Some Kw_mut
|
| "mut" -> Some Kw_mut
|
||||||
| "own" -> Some Kw_own
|
| "own" -> Some Kw_own
|
||||||
|
| "self" -> Some Kw_self
|
||||||
| "affine" -> Some Kw_affine
|
| "affine" -> Some Kw_affine
|
||||||
| "copyable" -> Some Kw_copyable
|
| "copyable" -> Some Kw_copyable
|
||||||
| "effects" -> Some Kw_effects
|
| "effects" -> Some Kw_effects
|
||||||
@@ -87,6 +91,8 @@ let keyword = function
|
|||||||
| "else" -> Some Kw_else
|
| "else" -> Some Kw_else
|
||||||
| "match" -> Some Kw_match
|
| "match" -> Some Kw_match
|
||||||
| "scope" -> Some Kw_scope
|
| "scope" -> Some Kw_scope
|
||||||
|
| "crash" -> Some Kw_crash
|
||||||
|
| "test" -> Some Kw_test
|
||||||
| "true" -> Some Kw_true
|
| "true" -> Some Kw_true
|
||||||
| "false" -> Some Kw_false
|
| "false" -> Some Kw_false
|
||||||
| _ -> None
|
| _ -> None
|
||||||
@@ -107,6 +113,7 @@ let show_kind = function
|
|||||||
| Kw_let -> "let"
|
| Kw_let -> "let"
|
||||||
| Kw_mut -> "mut"
|
| Kw_mut -> "mut"
|
||||||
| Kw_own -> "own"
|
| Kw_own -> "own"
|
||||||
|
| Kw_self -> "self"
|
||||||
| Kw_affine -> "affine"
|
| Kw_affine -> "affine"
|
||||||
| Kw_copyable -> "copyable"
|
| Kw_copyable -> "copyable"
|
||||||
| Kw_effects -> "effects"
|
| Kw_effects -> "effects"
|
||||||
@@ -115,6 +122,8 @@ let show_kind = function
|
|||||||
| Kw_else -> "else"
|
| Kw_else -> "else"
|
||||||
| Kw_match -> "match"
|
| Kw_match -> "match"
|
||||||
| Kw_scope -> "scope"
|
| Kw_scope -> "scope"
|
||||||
|
| Kw_crash -> "crash"
|
||||||
|
| Kw_test -> "test"
|
||||||
| Kw_true -> "true"
|
| Kw_true -> "true"
|
||||||
| Kw_false -> "false"
|
| Kw_false -> "false"
|
||||||
| LParen -> "("
|
| LParen -> "("
|
||||||
@@ -152,6 +161,78 @@ let show_kind = function
|
|||||||
let show { kind; pos } =
|
let show { kind; pos } =
|
||||||
Printf.sprintf "%d:%d %s" pos.line pos.col (show_kind kind)
|
Printf.sprintf "%d:%d %s" pos.line pos.col (show_kind kind)
|
||||||
|
|
||||||
|
(* 모든 토큰 종류를 한 번씩 잇는 사슬. 아래 match가 전체 나열이므로 토큰을
|
||||||
|
추가하면 컴파일러가 여기를 지적한다 — 목록이 조용히 낡지 않는다.
|
||||||
|
문법 문서의 어휘 절이 이 나열에서 생성된다. *)
|
||||||
|
let next_kind = function
|
||||||
|
| Ident _ -> Some (Int "")
|
||||||
|
| Int _ -> Some (Str "")
|
||||||
|
| Str _ -> Some Kw_pub
|
||||||
|
| Kw_pub -> Some Kw_fn
|
||||||
|
| Kw_fn -> Some Kw_struct
|
||||||
|
| Kw_struct -> Some Kw_enum
|
||||||
|
| Kw_enum -> Some Kw_capability
|
||||||
|
| Kw_capability -> Some Kw_const
|
||||||
|
| Kw_const -> Some Kw_import
|
||||||
|
| Kw_import -> Some Kw_as
|
||||||
|
| Kw_as -> Some Kw_reexport
|
||||||
|
| Kw_reexport -> Some Kw_let
|
||||||
|
| Kw_let -> Some Kw_mut
|
||||||
|
| Kw_mut -> Some Kw_own
|
||||||
|
| Kw_own -> Some Kw_self
|
||||||
|
| Kw_self -> Some Kw_affine
|
||||||
|
| Kw_affine -> Some Kw_copyable
|
||||||
|
| Kw_copyable -> Some Kw_effects
|
||||||
|
| Kw_effects -> Some Kw_return
|
||||||
|
| Kw_return -> Some Kw_if
|
||||||
|
| Kw_if -> Some Kw_else
|
||||||
|
| Kw_else -> Some Kw_match
|
||||||
|
| Kw_match -> Some Kw_scope
|
||||||
|
| Kw_scope -> Some Kw_crash
|
||||||
|
| Kw_crash -> Some Kw_test
|
||||||
|
| Kw_test -> Some Kw_true
|
||||||
|
| Kw_true -> Some Kw_false
|
||||||
|
| Kw_false -> Some LParen
|
||||||
|
| LParen -> Some RParen
|
||||||
|
| RParen -> Some LBrace
|
||||||
|
| LBrace -> Some RBrace
|
||||||
|
| RBrace -> Some LBracket
|
||||||
|
| LBracket -> Some RBracket
|
||||||
|
| RBracket -> Some Comma
|
||||||
|
| Comma -> Some Colon
|
||||||
|
| Colon -> Some Dot
|
||||||
|
| Dot -> Some Arrow
|
||||||
|
| Arrow -> Some FatArrow
|
||||||
|
| FatArrow -> Some Question
|
||||||
|
| Question -> Some Underscore
|
||||||
|
| Underscore -> Some Eq
|
||||||
|
| Eq -> Some EqEq
|
||||||
|
| EqEq -> Some Bang
|
||||||
|
| Bang -> Some BangEq
|
||||||
|
| BangEq -> Some Lt
|
||||||
|
| Lt -> Some Le
|
||||||
|
| Le -> Some Gt
|
||||||
|
| Gt -> Some Ge
|
||||||
|
| Ge -> Some Plus
|
||||||
|
| Plus -> Some Minus
|
||||||
|
| Minus -> Some Star
|
||||||
|
| Star -> Some Slash
|
||||||
|
| Slash -> Some Percent
|
||||||
|
| Percent -> Some AmpAmp
|
||||||
|
| AmpAmp -> Some PipePipe
|
||||||
|
| PipePipe -> Some Pipe
|
||||||
|
| Pipe -> Some Newline
|
||||||
|
| Newline -> Some Eof
|
||||||
|
| Eof -> None
|
||||||
|
|
||||||
|
let all_kinds =
|
||||||
|
let rec go k acc =
|
||||||
|
match next_kind k with
|
||||||
|
| None -> List.rev (k :: acc)
|
||||||
|
| Some n -> go n (k :: acc)
|
||||||
|
in
|
||||||
|
go (Ident "") []
|
||||||
|
|
||||||
(* 줄 끝에서 문 구분자를 삽입할지 결정한다 (grammar.ebnf 어휘 절).
|
(* 줄 끝에서 문 구분자를 삽입할지 결정한다 (grammar.ebnf 어휘 절).
|
||||||
값으로 끝날 수 있는 토큰 뒤에서만 삽입하므로, 연산자나 여는 괄호로
|
값으로 끝날 수 있는 토큰 뒤에서만 삽입하므로, 연산자나 여는 괄호로
|
||||||
끝나는 줄은 다음 줄로 이어진다. *)
|
끝나는 줄은 다음 줄로 이어진다. *)
|
||||||
|
|||||||
+363
-43
@@ -12,8 +12,11 @@ module T = Types
|
|||||||
type error = { pos : Token.pos; msg : string }
|
type error = { pos : Token.pos; msg : string }
|
||||||
|
|
||||||
type scheme = {
|
type scheme = {
|
||||||
s_gen : string list; (* 타입 파라미터 이름 (effect 파라미터는 제외) *)
|
s_gen : string list; (* 타입 파라미터 *)
|
||||||
s_params : T.t list;
|
s_eff_gen : string list; (* effect 파라미터 *)
|
||||||
|
(* (own, 타입). 무표기는 빌림 *)
|
||||||
|
s_params : (bool * T.t) list;
|
||||||
|
s_eff : T.eff; (* 이 함수를 부르면 수행되는 effect *)
|
||||||
s_ret : T.t;
|
s_ret : T.t;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,13 +27,43 @@ type env = {
|
|||||||
fns : (string, scheme) Hashtbl.t;
|
fns : (string, scheme) Hashtbl.t;
|
||||||
consts : (string, T.t) Hashtbl.t;
|
consts : (string, T.t) Hashtbl.t;
|
||||||
ctors : (string, string) Hashtbl.t; (* variant -> enum *)
|
ctors : (string, string) Hashtbl.t; (* variant -> enum *)
|
||||||
|
(* 가져온 모듈의 별칭. `Alias.x`는 "Alias.x"라는 하나의 키로 찾는다 —
|
||||||
|
별칭은 식별자에 쓸 수 없는 점(.)을 포함하므로 지역 이름과 충돌하지 않는다. *)
|
||||||
|
aliases : string list;
|
||||||
mutable locals : (string * T.t) list list;
|
mutable locals : (string * T.t) list list;
|
||||||
mutable ret : T.t; (* 현재 함수의 선언된 반환 타입 *)
|
mutable ret : T.t; (* 현재 함수의 선언된 반환 타입 *)
|
||||||
|
(* 현재 본문이 수행한 effect. 위치를 같이 들고 다녀야 "어디서 수행했는지"를
|
||||||
|
말할 수 있다. 클로저에 들어가면 저장하고 비운다 — 클로저의 effect는
|
||||||
|
정의한 자리가 아니라 부르는 자리에서 일어난다. *)
|
||||||
|
mutable performed : (T.atom * Token.pos) list;
|
||||||
|
(* 이 본문에서 모르는 것을 만났는가. 과잉 선언 판정에만 쓴다 — 외부 타입의
|
||||||
|
메서드는 effect를 알 수 없으므로 "수행하지 않았다"고 말할 근거가 없다. *)
|
||||||
|
mutable saw_unknown : bool;
|
||||||
mutable errors : error list;
|
mutable errors : error list;
|
||||||
}
|
}
|
||||||
|
|
||||||
let err env pos msg = env.errors <- { pos; msg } :: env.errors
|
let err env pos msg = env.errors <- { pos; msg } :: env.errors
|
||||||
|
|
||||||
|
let expr_pos (e : expr) : Token.pos =
|
||||||
|
match e with
|
||||||
|
| E_lit (_, p)
|
||||||
|
| E_ident (_, p)
|
||||||
|
| E_list (_, p)
|
||||||
|
| E_struct { pos = p; _ }
|
||||||
|
| E_if { pos = p; _ }
|
||||||
|
| E_match { pos = p; _ }
|
||||||
|
| E_scope { pos = p; _ }
|
||||||
|
| E_call { pos = p; _ }
|
||||||
|
| E_field { pos = p; _ }
|
||||||
|
| E_inst { pos = p; _ }
|
||||||
|
| E_try { pos = p; _ }
|
||||||
|
| E_unary { pos = p; _ }
|
||||||
|
| E_binary { pos = p; _ }
|
||||||
|
| E_crash { pos = p; _ } ->
|
||||||
|
p
|
||||||
|
| E_closure c -> c.cl_pos
|
||||||
|
| E_block b -> b.block_pos
|
||||||
|
|
||||||
let mismatch env pos expected got what =
|
let mismatch env pos expected got what =
|
||||||
err env pos
|
err env pos
|
||||||
(Printf.sprintf "%s: %s이(가) 필요한데 %s입니다" what (T.show expected) (T.show got))
|
(Printf.sprintf "%s: %s이(가) 필요한데 %s입니다" what (T.show expected) (T.show got))
|
||||||
@@ -55,17 +88,26 @@ let lookup env n =
|
|||||||
(* Ast.ty -> Types.t *)
|
(* Ast.ty -> Types.t *)
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
let conv_eff_atom (a : Ast.eff_atom) : T.eff =
|
||||||
|
match a with
|
||||||
|
| Eff_var v -> [ T.A_var v ]
|
||||||
|
| Eff_set names -> List.map (fun { cap; meth } -> T.A_name (cap, meth)) names
|
||||||
|
|
||||||
|
let conv_eff_result (atoms : Ast.eff_result) : T.eff =
|
||||||
|
T.eff_resolve (List.concat_map conv_eff_atom atoms)
|
||||||
|
|
||||||
let rec conv env (gen : string list) (t : Ast.ty) : T.t =
|
let rec conv env (gen : string list) (t : Ast.ty) : T.t =
|
||||||
match t with
|
match t with
|
||||||
| T_named { name; args; _ } -> (
|
| T_named { modl; name; args; _ } -> (
|
||||||
let args =
|
let args =
|
||||||
List.filter_map
|
List.filter_map
|
||||||
(function TA_ty t -> Some (conv env gen t) | TA_eff _ -> None)
|
(function TA_ty t -> Some (conv env gen t) | TA_eff _ -> None)
|
||||||
args
|
args
|
||||||
in
|
in
|
||||||
if List.mem name gen then T.TVar name
|
let name = match modl with Some a -> a ^ "." ^ name | None -> name in
|
||||||
|
if modl = None && List.mem name gen then T.TVar name
|
||||||
else
|
else
|
||||||
match name with
|
match if modl = None then name else "" with
|
||||||
| "Int" -> T.TInt
|
| "Int" -> T.TInt
|
||||||
| "Bool" -> T.TBool
|
| "Bool" -> T.TBool
|
||||||
| "String" -> T.TString
|
| "String" -> T.TString
|
||||||
@@ -77,11 +119,15 @@ let rec conv env (gen : string list) (t : Ast.ty) : T.t =
|
|||||||
|| Hashtbl.mem env.enums name || Hashtbl.mem env.caps name
|
|| Hashtbl.mem env.enums name || Hashtbl.mem env.caps name
|
||||||
then T.TCon (name, args)
|
then T.TCon (name, args)
|
||||||
else T.TUnknown)
|
else T.TUnknown)
|
||||||
| T_fn { affine; params; ret; _ } ->
|
| T_fn { affine; params; eff; ret; _ } ->
|
||||||
T.TFn
|
T.TFn
|
||||||
{
|
{
|
||||||
affine;
|
affine;
|
||||||
params = List.map (conv env gen) params;
|
params =
|
||||||
|
List.map
|
||||||
|
(fun (p : fn_param_ty) -> (p.pt_own, conv env gen p.pt_ty))
|
||||||
|
params;
|
||||||
|
eff = (match eff with None -> [] | Some a -> conv_eff_atom a);
|
||||||
ret = (match ret with None -> T.TUnit | Some r -> conv env gen r);
|
ret = (match ret with None -> T.TUnit | Some r -> conv env gen r);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,20 +137,34 @@ let scheme_of env (d : fn_decl) : scheme =
|
|||||||
(fun g -> if g.gp_effect then None else Some g.gp_name)
|
(fun g -> if g.gp_effect then None else Some g.gp_name)
|
||||||
d.fn_gen
|
d.fn_gen
|
||||||
in
|
in
|
||||||
|
let egen =
|
||||||
|
List.filter_map
|
||||||
|
(fun g -> if g.gp_effect then Some g.gp_name else None)
|
||||||
|
d.fn_gen
|
||||||
|
in
|
||||||
{
|
{
|
||||||
s_gen = gen;
|
s_gen = gen;
|
||||||
s_params = List.map (fun p -> conv env gen p.p_ty) d.fn_params;
|
s_eff_gen = egen;
|
||||||
|
s_params = List.map (fun p -> (p.p_own, conv env gen p.p_ty)) d.fn_params;
|
||||||
|
s_eff =
|
||||||
|
(match d.fn_eff with None -> [] | Some atoms -> conv_eff_result atoms);
|
||||||
s_ret = (match d.fn_ret with None -> T.TUnit | Some r -> conv env gen r);
|
s_ret = (match d.fn_ret with None -> T.TUnit | Some r -> conv env gen r);
|
||||||
}
|
}
|
||||||
|
|
||||||
(* 호출 지점 인스턴스화: 타입 파라미터마다 새 미지수 *)
|
(* 호출 지점 인스턴스화: 타입 파라미터마다 새 미지수 *)
|
||||||
let instantiate (s : scheme) =
|
let instantiate (s : scheme) =
|
||||||
let sub = List.map (fun v -> (v, T.fresh ())) s.s_gen in
|
let sub = List.map (fun v -> (v, T.fresh ())) s.s_gen in
|
||||||
(List.map (T.subst sub) s.s_params, T.subst sub s.s_ret)
|
let esub = List.map (fun v -> (v, T.fresh_eff ())) s.s_eff_gen in
|
||||||
|
( List.map (fun (o, t) -> (o, T.subst sub esub t)) s.s_params,
|
||||||
|
T.subst_eff esub s.s_eff,
|
||||||
|
T.subst sub esub s.s_ret )
|
||||||
|
|
||||||
let instantiate_with (s : scheme) (args : T.t list) =
|
let instantiate_with (s : scheme) (args : T.t list) =
|
||||||
let sub = List.map2 (fun v a -> (v, a)) s.s_gen args in
|
let sub = List.map2 (fun v a -> (v, a)) s.s_gen args in
|
||||||
(List.map (T.subst sub) s.s_params, T.subst sub s.s_ret)
|
let esub = List.map (fun v -> (v, T.fresh_eff ())) s.s_eff_gen in
|
||||||
|
( List.map (fun (o, t) -> (o, T.subst sub esub t)) s.s_params,
|
||||||
|
T.subst_eff esub s.s_eff,
|
||||||
|
T.subst sub esub s.s_ret )
|
||||||
|
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
(* 내장 생성자 *)
|
(* 내장 생성자 *)
|
||||||
@@ -128,6 +188,12 @@ let builtin_ctor = function
|
|||||||
|
|
||||||
let rec infer env (e : expr) : T.t =
|
let rec infer env (e : expr) : T.t =
|
||||||
match e with
|
match e with
|
||||||
|
(* crash는 돌아오지 않는다. 타입은 Never이고 어떤 자리에도 놓인다 *)
|
||||||
|
| E_crash { msg; pos } ->
|
||||||
|
let t = infer env msg in
|
||||||
|
if not (T.unify t T.TString) then
|
||||||
|
mismatch env pos T.TString t "crash의 메시지";
|
||||||
|
T.TNever
|
||||||
| E_lit (L_int _, _) -> T.TInt
|
| E_lit (L_int _, _) -> T.TInt
|
||||||
| E_lit (L_str _, _) -> T.TString
|
| E_lit (L_str _, _) -> T.TString
|
||||||
| E_lit (L_bool _, _) -> T.TBool
|
| E_lit (L_bool _, _) -> T.TBool
|
||||||
@@ -144,12 +210,14 @@ let rec infer env (e : expr) : T.t =
|
|||||||
| _ -> (
|
| _ -> (
|
||||||
match Hashtbl.find_opt env.fns n with
|
match Hashtbl.find_opt env.fns n with
|
||||||
| Some s ->
|
| Some s ->
|
||||||
let params, ret = instantiate s in
|
let params, eff, ret = instantiate s in
|
||||||
T.TFn { affine = false; params; ret }
|
T.TFn { affine = false; params; eff; ret }
|
||||||
| None -> (
|
| None -> (
|
||||||
match Hashtbl.find_opt env.ctors n with
|
match Hashtbl.find_opt env.ctors n with
|
||||||
| Some enum -> nullary_ctor env enum n
|
| Some enum -> nullary_ctor env enum n
|
||||||
| None -> T.TUnknown)))))
|
| None ->
|
||||||
|
env.saw_unknown <- true;
|
||||||
|
T.TUnknown)))))
|
||||||
| E_list (xs, pos) ->
|
| E_list (xs, pos) ->
|
||||||
let elem = T.fresh () in
|
let elem = T.fresh () in
|
||||||
List.iter
|
List.iter
|
||||||
@@ -187,6 +255,7 @@ let rec infer env (e : expr) : T.t =
|
|||||||
pop env)
|
pop env)
|
||||||
arms;
|
arms;
|
||||||
if arms = [] then err env pos "match에 팔이 없습니다";
|
if arms = [] then err env pos "match에 팔이 없습니다";
|
||||||
|
check_exhaustive env s arms pos;
|
||||||
result
|
result
|
||||||
| E_scope { name; parent; body; pos } ->
|
| E_scope { name; parent; body; pos } ->
|
||||||
(match lookup env parent with
|
(match lookup env parent with
|
||||||
@@ -247,6 +316,40 @@ let rec infer env (e : expr) : T.t =
|
|||||||
if not (T.unify a b) then mismatch env pos a b "같은 타입끼리만 비교할 수 있습니다";
|
if not (T.unify a b) then mismatch env pos a b "같은 타입끼리만 비교할 수 있습니다";
|
||||||
T.TBool)
|
T.TBool)
|
||||||
|
|
||||||
|
(* exhaustiveness: 철학 1의 대표 항목이자 interface hash가 enum 본문을
|
||||||
|
입력으로 삼는 이유다 *)
|
||||||
|
and check_exhaustive env scrutinee arms pos =
|
||||||
|
let eenv : Exhaust.env =
|
||||||
|
{
|
||||||
|
variants =
|
||||||
|
(fun name args ->
|
||||||
|
match Hashtbl.find_opt env.enums name with
|
||||||
|
| None -> None
|
||||||
|
| Some (gen, variants) ->
|
||||||
|
let sub =
|
||||||
|
try List.map2 (fun v a -> (v, a)) gen args
|
||||||
|
with Invalid_argument _ -> []
|
||||||
|
in
|
||||||
|
Some
|
||||||
|
(List.map
|
||||||
|
(fun (n, tys) -> (n, List.map (T.subst sub []) tys))
|
||||||
|
variants));
|
||||||
|
is_ctor =
|
||||||
|
(fun n ->
|
||||||
|
Hashtbl.mem env.ctors n || List.mem n [ "Ok"; "Err"; "Some"; "None" ]);
|
||||||
|
}
|
||||||
|
in
|
||||||
|
let r = Exhaust.check eenv scrutinee (List.map (fun a -> a.arm_pat) arms) in
|
||||||
|
(match r.missing with
|
||||||
|
| None -> ()
|
||||||
|
| Some w -> err env pos (Printf.sprintf "match가 모든 경우를 덮지 않습니다 (빠진 경우: %s)" w));
|
||||||
|
List.iter
|
||||||
|
(fun i ->
|
||||||
|
match List.nth_opt arms i with
|
||||||
|
| Some a -> err env a.arm_pos "이 팔은 앞의 팔들에 가려 도달할 수 없습니다"
|
||||||
|
| None -> ())
|
||||||
|
r.unreachable
|
||||||
|
|
||||||
and nullary_ctor env enum name =
|
and nullary_ctor env enum name =
|
||||||
match Hashtbl.find_opt env.enums enum with
|
match Hashtbl.find_opt env.enums enum with
|
||||||
| None -> T.TUnknown
|
| None -> T.TUnknown
|
||||||
@@ -268,7 +371,7 @@ and infer_struct env name fields pos =
|
|||||||
match List.assoc_opt fname decl_fields with
|
match List.assoc_opt fname decl_fields with
|
||||||
| None -> err env pos (Printf.sprintf "%s에 %s 필드가 없습니다" name fname)
|
| None -> err env pos (Printf.sprintf "%s에 %s 필드가 없습니다" name fname)
|
||||||
| Some ft ->
|
| Some ft ->
|
||||||
let want = T.subst sub ft in
|
let want = T.subst sub [] ft in
|
||||||
let got = infer env fe in
|
let got = infer env fe in
|
||||||
if not (T.unify want got) then
|
if not (T.unify want got) then
|
||||||
mismatch env pos want got (Printf.sprintf "%s.%s 필드" name fname))
|
mismatch env pos want got (Printf.sprintf "%s.%s 필드" name fname))
|
||||||
@@ -285,36 +388,69 @@ and infer_closure env (c : closure) (expected : T.t option) =
|
|||||||
match Option.map T.resolve expected with
|
match Option.map T.resolve expected with
|
||||||
| Some (T.TFn { params; ret; _ })
|
| Some (T.TFn { params; ret; _ })
|
||||||
when List.length params = List.length c.cl_params ->
|
when List.length params = List.length c.cl_params ->
|
||||||
(List.map Option.some params, Some ret)
|
(List.map (fun (o, t) -> Some (o, t)) params, Some ret)
|
||||||
| _ -> (List.map (fun _ -> None) c.cl_params, None)
|
| _ -> (List.map (fun _ -> None) c.cl_params, None)
|
||||||
in
|
in
|
||||||
push env;
|
push env;
|
||||||
let param_tys =
|
let param_tys =
|
||||||
List.map2
|
List.map2
|
||||||
(fun (n, ann) exp ->
|
(fun (p : cl_param) exp ->
|
||||||
let t =
|
let t =
|
||||||
match ann with
|
match p.cp_ty with
|
||||||
| Some a -> conv env [] a
|
| Some a -> conv env [] a
|
||||||
| None -> ( match exp with Some t -> t | None -> T.TUnknown)
|
| None -> ( match exp with Some (_, t) -> t | None -> T.TUnknown)
|
||||||
in
|
in
|
||||||
bind env n t;
|
(* 기대 타입이 소유를 말하는데 리터럴이 안 적었으면 오류다.
|
||||||
t)
|
반대도 오류다 — 빌리는 자리에 소유를 주장하면 빌린 값을 소비한다.
|
||||||
|
방향을 다루려면 부분 타입이 필요하고 우리에겐 없다. *)
|
||||||
|
(match exp with
|
||||||
|
| Some (o, _) when o <> p.cp_own ->
|
||||||
|
err env c.cl_pos
|
||||||
|
(Printf.sprintf "클로저 파라미터 %s의 소유권이 기대와 다릅니다 (기대: %s, 적힌 것: %s)"
|
||||||
|
p.cp_name
|
||||||
|
(if o then "own" else "빌림")
|
||||||
|
(if p.cp_own then "own" else "빌림"))
|
||||||
|
| _ -> ());
|
||||||
|
bind env p.cp_name t;
|
||||||
|
(p.cp_own, t))
|
||||||
c.cl_params expected_params
|
c.cl_params expected_params
|
||||||
in
|
in
|
||||||
let declared_ret = Option.map (conv env []) c.cl_ret in
|
let declared_ret = Option.map (conv env []) c.cl_ret in
|
||||||
let saved = env.ret in
|
let saved_ret = env.ret in
|
||||||
env.ret <-
|
env.ret <-
|
||||||
(match declared_ret with
|
(match declared_ret with
|
||||||
| Some t -> t
|
| Some t -> t
|
||||||
| None -> ( match expected_ret with Some t -> t | None -> T.TUnknown));
|
| None -> ( match expected_ret with Some t -> t | None -> T.TUnknown));
|
||||||
|
(* 클로저 본문의 effect는 바깥 함수가 수행하는 것이 아니다. 저장하고 비운다. *)
|
||||||
|
let saved_perf = env.performed in
|
||||||
|
env.performed <- [];
|
||||||
let body = infer_block env c.cl_body in
|
let body = infer_block env c.cl_body in
|
||||||
|
let inner = env.performed in
|
||||||
|
env.performed <- saved_perf;
|
||||||
|
let declared_eff = Option.map conv_eff_atom c.cl_eff in
|
||||||
|
let eff =
|
||||||
|
match declared_eff with
|
||||||
|
| Some d ->
|
||||||
|
(* 클로저가 effects를 명시했으면 본문이 그 안에 들어야 한다 *)
|
||||||
|
List.iter
|
||||||
|
(fun (a, pos) ->
|
||||||
|
List.iter
|
||||||
|
(fun m ->
|
||||||
|
err env pos
|
||||||
|
(Printf.sprintf "클로저가 선언하지 않은 effect %s을(를) 수행합니다 (선언: %s)"
|
||||||
|
(T.atom_show m) (T.eff_show d)))
|
||||||
|
(T.eff_missing ~declared:d ~performed:[ a ]))
|
||||||
|
inner;
|
||||||
|
d
|
||||||
|
| None -> T.eff_resolve (List.map fst inner)
|
||||||
|
in
|
||||||
(match declared_ret with
|
(match declared_ret with
|
||||||
| Some t when not (T.unify t body) -> mismatch env c.cl_pos t body "클로저의 반환"
|
| Some t when not (T.unify t body) -> mismatch env c.cl_pos t body "클로저의 반환"
|
||||||
| _ -> ());
|
| _ -> ());
|
||||||
let ret = match declared_ret with Some t -> t | None -> body in
|
let ret = match declared_ret with Some t -> t | None -> body in
|
||||||
env.ret <- saved;
|
env.ret <- saved_ret;
|
||||||
pop env;
|
pop env;
|
||||||
T.TFn { affine = false; params = param_tys; ret }
|
T.TFn { affine = false; params = param_tys; eff; ret }
|
||||||
|
|
||||||
and infer_call env callee args pos =
|
and infer_call env callee args pos =
|
||||||
let fn_ty =
|
let fn_ty =
|
||||||
@@ -324,12 +460,20 @@ and infer_call env callee args pos =
|
|||||||
| Some enum -> Some (ctor_fn env enum n)
|
| Some enum -> Some (ctor_fn env enum n)
|
||||||
| None -> (
|
| None -> (
|
||||||
match builtin_ctor n with
|
match builtin_ctor n with
|
||||||
| Some (params, ret) -> Some (T.TFn { affine = false; params; ret })
|
| Some (params, ret) ->
|
||||||
|
Some
|
||||||
|
(T.TFn
|
||||||
|
{
|
||||||
|
affine = false;
|
||||||
|
params = List.map (fun t -> (false, t)) params;
|
||||||
|
eff = [];
|
||||||
|
ret;
|
||||||
|
})
|
||||||
| None -> (
|
| None -> (
|
||||||
match Hashtbl.find_opt env.fns n with
|
match Hashtbl.find_opt env.fns n with
|
||||||
| Some s ->
|
| Some s ->
|
||||||
let params, ret = instantiate s in
|
let params, eff, ret = instantiate s in
|
||||||
Some (T.TFn { affine = false; params; ret })
|
Some (T.TFn { affine = false; params; eff; ret })
|
||||||
| None -> None)))
|
| None -> None)))
|
||||||
| _ -> (
|
| _ -> (
|
||||||
match T.resolve (infer env callee) with
|
match T.resolve (infer env callee) with
|
||||||
@@ -338,9 +482,13 @@ and infer_call env callee args pos =
|
|||||||
in
|
in
|
||||||
match fn_ty with
|
match fn_ty with
|
||||||
| None ->
|
| None ->
|
||||||
|
env.saw_unknown <- true;
|
||||||
List.iter (fun a -> ignore (infer env a)) args;
|
List.iter (fun a -> ignore (infer env a)) args;
|
||||||
T.TUnknown
|
T.TUnknown
|
||||||
| Some (T.TFn { params; ret; _ }) ->
|
| Some (T.TFn { params; eff; ret; _ }) ->
|
||||||
|
List.iter
|
||||||
|
(fun a -> env.performed <- (a, pos) :: env.performed)
|
||||||
|
(T.eff_resolve eff);
|
||||||
if List.length params <> List.length args then (
|
if List.length params <> List.length args then (
|
||||||
err env pos
|
err env pos
|
||||||
(Printf.sprintf "인자 %d개가 필요한데 %d개가 주어졌습니다" (List.length params)
|
(Printf.sprintf "인자 %d개가 필요한데 %d개가 주어졌습니다" (List.length params)
|
||||||
@@ -348,13 +496,28 @@ and infer_call env callee args pos =
|
|||||||
List.iter (fun a -> ignore (infer env a)) args)
|
List.iter (fun a -> ignore (infer env a)) args)
|
||||||
else
|
else
|
||||||
List.iter2
|
List.iter2
|
||||||
(fun p a ->
|
(fun (_, p) a ->
|
||||||
let got =
|
let got =
|
||||||
match a with
|
match a with
|
||||||
| E_closure c -> infer_closure env c (Some p)
|
| E_closure c -> infer_closure env c (Some p)
|
||||||
| _ -> infer env a
|
| _ -> infer env a
|
||||||
in
|
in
|
||||||
if not (T.unify p got) then mismatch env pos p got "인자")
|
(* 먼저 unify한다. 결정 위치의 effect 변수는 여기서 인자의
|
||||||
|
effect로 묶인다 — 제약이 아니라 해소다. 그 뒤에 남는 차이만이
|
||||||
|
진짜 위반이다. *)
|
||||||
|
if not (T.unify p got) then mismatch env pos p got "인자";
|
||||||
|
match (T.resolve p, T.resolve got) with
|
||||||
|
| T.TFn pf, T.TFn gf ->
|
||||||
|
let missing =
|
||||||
|
T.eff_missing ~declared:pf.eff ~performed:gf.eff
|
||||||
|
in
|
||||||
|
if missing <> [] then
|
||||||
|
err env pos
|
||||||
|
(Printf.sprintf
|
||||||
|
"넘긴 함수가 %s을(를) 수행하는데 파라미터가 허용한 effect는 %s입니다"
|
||||||
|
(String.concat ", " (List.map T.atom_show missing))
|
||||||
|
(T.eff_show pf.eff))
|
||||||
|
| _ -> ())
|
||||||
params args;
|
params args;
|
||||||
ret
|
ret
|
||||||
| Some _ -> T.TUnknown
|
| Some _ -> T.TUnknown
|
||||||
@@ -366,12 +529,52 @@ and ctor_fn env enum name =
|
|||||||
let sub = List.map (fun v -> (v, T.fresh ())) gen in
|
let sub = List.map (fun v -> (v, T.fresh ())) gen in
|
||||||
let params =
|
let params =
|
||||||
match List.assoc_opt name variants with
|
match List.assoc_opt name variants with
|
||||||
| Some ts -> List.map (T.subst sub) ts
|
| Some ts -> List.map (fun t -> (false, T.subst sub [] t)) ts
|
||||||
| None -> []
|
| None -> []
|
||||||
in
|
in
|
||||||
T.TFn { affine = false; params; ret = T.TCon (enum, List.map snd sub) }
|
T.TFn
|
||||||
|
{
|
||||||
|
affine = false;
|
||||||
|
params;
|
||||||
|
eff = [];
|
||||||
|
ret = T.TCon (enum, List.map snd sub);
|
||||||
|
}
|
||||||
|
|
||||||
and infer_field env obj name pos =
|
and infer_field env obj name pos =
|
||||||
|
(* capability 메서드는 값을 통해서만 부를 수 있다. 타입 이름으로 부를 수 있으면
|
||||||
|
capability 없이 effect를 수행하게 되어 보안 정리 (i)이 무너진다. *)
|
||||||
|
(match obj with
|
||||||
|
| E_ident (n, _) when lookup env n = None && Hashtbl.mem env.caps n ->
|
||||||
|
err env pos
|
||||||
|
(Printf.sprintf
|
||||||
|
"capability %s의 메서드는 값을 통해서만 부를 수 있습니다 (%s를 파라미터로 받아야 합니다)" n n)
|
||||||
|
| _ -> ());
|
||||||
|
match obj with
|
||||||
|
| E_ident (a, _) when lookup env a = None && List.mem a env.aliases -> (
|
||||||
|
(* 모듈 별칭을 통한 접근. 가져온 표면은 "Alias.name" 키로 들어와 있다. *)
|
||||||
|
let key = a ^ "." ^ name in
|
||||||
|
let unknown () =
|
||||||
|
err env pos (Printf.sprintf "%s에 %s이(가) 없습니다" a name);
|
||||||
|
T.TUnknown
|
||||||
|
in
|
||||||
|
match Hashtbl.find_opt env.consts key with
|
||||||
|
| Some t -> t
|
||||||
|
| None -> (
|
||||||
|
match Hashtbl.find_opt env.fns key with
|
||||||
|
| Some s ->
|
||||||
|
let params, eff, ret = instantiate s in
|
||||||
|
T.TFn { affine = false; params; eff; ret }
|
||||||
|
| None -> (
|
||||||
|
match Hashtbl.find_opt env.ctors key with
|
||||||
|
| Some enum -> (
|
||||||
|
match Hashtbl.find_opt env.enums enum with
|
||||||
|
| Some (_, variants)
|
||||||
|
when List.assoc_opt key variants = Some [] ->
|
||||||
|
nullary_ctor env enum key
|
||||||
|
| Some _ -> ctor_fn env enum key
|
||||||
|
| None -> T.TUnknown)
|
||||||
|
| None -> unknown ())))
|
||||||
|
| _ -> (
|
||||||
let t = infer env obj in
|
let t = infer env obj in
|
||||||
match T.resolve t with
|
match T.resolve t with
|
||||||
| T.TUnknown -> T.TUnknown
|
| T.TUnknown -> T.TUnknown
|
||||||
@@ -380,7 +583,7 @@ and infer_field env obj name pos =
|
|||||||
| Some (gen, fields) -> (
|
| Some (gen, fields) -> (
|
||||||
let sub = List.map2 (fun v a -> (v, a)) gen (adjust gen args) in
|
let sub = List.map2 (fun v a -> (v, a)) gen (adjust gen args) in
|
||||||
match List.assoc_opt name fields with
|
match List.assoc_opt name fields with
|
||||||
| Some ft -> T.subst sub ft
|
| Some ft -> T.subst sub [] ft
|
||||||
| None ->
|
| None ->
|
||||||
err env pos (Printf.sprintf "%s에 %s 필드가 없습니다" cname name);
|
err env pos (Printf.sprintf "%s에 %s 필드가 없습니다" cname name);
|
||||||
T.TUnknown)
|
T.TUnknown)
|
||||||
@@ -389,8 +592,8 @@ and infer_field env obj name pos =
|
|||||||
| Some methods -> (
|
| Some methods -> (
|
||||||
match List.assoc_opt name methods with
|
match List.assoc_opt name methods with
|
||||||
| Some s ->
|
| Some s ->
|
||||||
let params, ret = instantiate s in
|
let params, eff, ret = instantiate s in
|
||||||
T.TFn { affine = false; params; ret }
|
T.TFn { affine = false; params; eff; ret }
|
||||||
| None ->
|
| None ->
|
||||||
err env pos
|
err env pos
|
||||||
(Printf.sprintf "capability %s에 %s 메서드가 없습니다" cname name);
|
(Printf.sprintf "capability %s에 %s 메서드가 없습니다" cname name);
|
||||||
@@ -398,7 +601,7 @@ and infer_field env obj name pos =
|
|||||||
| None -> T.TUnknown))
|
| None -> T.TUnknown))
|
||||||
| other ->
|
| other ->
|
||||||
err env pos (Printf.sprintf "%s에는 필드가 없습니다" (T.show other));
|
err env pos (Printf.sprintf "%s에는 필드가 없습니다" (T.show other));
|
||||||
T.TUnknown
|
T.TUnknown)
|
||||||
|
|
||||||
and adjust gen args =
|
and adjust gen args =
|
||||||
let n = List.length gen in
|
let n = List.length gen in
|
||||||
@@ -428,8 +631,8 @@ and infer_inst env callee args pos =
|
|||||||
(List.length s.s_gen) (List.length tys));
|
(List.length s.s_gen) (List.length tys));
|
||||||
T.TUnknown)
|
T.TUnknown)
|
||||||
else
|
else
|
||||||
let params, ret = instantiate_with s tys in
|
let params, eff, ret = instantiate_with s tys in
|
||||||
T.TFn { affine = false; params; ret })
|
T.TFn { affine = false; params; eff; ret })
|
||||||
| _ -> T.TUnknown
|
| _ -> T.TUnknown
|
||||||
|
|
||||||
and check_pattern env (scrutinee : T.t) (p : pattern) =
|
and check_pattern env (scrutinee : T.t) (p : pattern) =
|
||||||
@@ -448,7 +651,8 @@ and check_pattern env (scrutinee : T.t) (p : pattern) =
|
|||||||
| Some enum ->
|
| Some enum ->
|
||||||
check_ctor env scrutinee enum n [] Token.{ line = 0; col = 0 }
|
check_ctor env scrutinee enum n [] Token.{ line = 0; col = 0 }
|
||||||
| None -> bind env n scrutinee)
|
| None -> bind env n scrutinee)
|
||||||
| P_ctor { name; args; pos } -> (
|
| P_ctor { modl; name; args; pos } -> (
|
||||||
|
let name = match modl with Some a -> a ^ "." ^ name | None -> name in
|
||||||
match Hashtbl.find_opt env.ctors name with
|
match Hashtbl.find_opt env.ctors name with
|
||||||
| Some enum -> check_ctor env scrutinee enum name args pos
|
| Some enum -> check_ctor env scrutinee enum name args pos
|
||||||
| None -> List.iter (check_pattern env T.TUnknown) args)
|
| None -> List.iter (check_pattern env T.TUnknown) args)
|
||||||
@@ -466,7 +670,7 @@ and check_ctor env scrutinee enum name args pos =
|
|||||||
in
|
in
|
||||||
if List.length fields = List.length args then
|
if List.length fields = List.length args then
|
||||||
List.iter2
|
List.iter2
|
||||||
(fun ft ap -> check_pattern env (T.subst sub ft) ap)
|
(fun ft ap -> check_pattern env (T.subst sub [] ft) ap)
|
||||||
fields args
|
fields args
|
||||||
|
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
@@ -484,6 +688,17 @@ and infer_block env (b : block) : T.t =
|
|||||||
go b.stmts
|
go b.stmts
|
||||||
|
|
||||||
and check_stmt env = function
|
and check_stmt env = function
|
||||||
|
(* 꼬리가 아닌 자리의 식은 값을 남기면 안 된다. 남긴 값은 버려지는데,
|
||||||
|
그 값이 Result면 실패가 조용히 사라진다 — 철학 1과 정면으로 부딪힌다.
|
||||||
|
일부러 버리려면 let _ = 로 적는다. 버린다는 사실이 코드에 보여야 한다. *)
|
||||||
|
| S_expr e -> (
|
||||||
|
let t = infer env e in
|
||||||
|
match T.resolve t with
|
||||||
|
| T.TUnit | T.TUnknown | T.TNever -> ()
|
||||||
|
| other ->
|
||||||
|
err env (expr_pos e)
|
||||||
|
(Printf.sprintf "이 식이 남기는 %s이(가) 버려집니다 (일부러 버리려면 let _ = 로 적으십시오)"
|
||||||
|
(T.show other)))
|
||||||
| S_let { pat; ty; value; pos; _ } ->
|
| S_let { pat; ty; value; pos; _ } ->
|
||||||
let declared = Option.map (conv env []) ty in
|
let declared = Option.map (conv env []) ty in
|
||||||
let got =
|
let got =
|
||||||
@@ -506,7 +721,6 @@ and check_stmt env = function
|
|||||||
let p = infer env place in
|
let p = infer env place in
|
||||||
let v = infer env value in
|
let v = infer env value in
|
||||||
if not (T.unify p v) then mismatch env pos p v "대입"
|
if not (T.unify p v) then mismatch env pos p v "대입"
|
||||||
| S_expr e -> ignore (infer env e)
|
|
||||||
|
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
(* 모듈 *)
|
(* 모듈 *)
|
||||||
@@ -527,13 +741,63 @@ let check_fn env (d : fn_decl) =
|
|||||||
match d.fn_ret with None -> T.TUnit | Some r -> conv env gen r
|
match d.fn_ret with None -> T.TUnit | Some r -> conv env gen r
|
||||||
in
|
in
|
||||||
env.ret <- declared;
|
env.ret <- declared;
|
||||||
|
env.performed <- [];
|
||||||
|
env.saw_unknown <- false;
|
||||||
let got = infer_block env body in
|
let got = infer_block env body in
|
||||||
if not (T.unify declared got) then
|
if not (T.unify declared got) then
|
||||||
mismatch env d.fn_pos declared got
|
mismatch env d.fn_pos declared got
|
||||||
(Printf.sprintf "%s의 본문이 남기는 값" d.fn_name);
|
(Printf.sprintf "%s의 본문이 남기는 값" d.fn_name);
|
||||||
|
(* 미선언 effect = compile error (철학 1).
|
||||||
|
수행한 자리를 알고 있으므로 그 자리에 진단을 붙인다. *)
|
||||||
|
let declared_eff =
|
||||||
|
match d.fn_eff with None -> [] | Some atoms -> conv_eff_result atoms
|
||||||
|
in
|
||||||
|
List.iter
|
||||||
|
(fun (a, pos) ->
|
||||||
|
match T.eff_missing ~declared:declared_eff ~performed:[ a ] with
|
||||||
|
| [] -> ()
|
||||||
|
| missing ->
|
||||||
|
List.iter
|
||||||
|
(fun m ->
|
||||||
|
err env pos
|
||||||
|
(Printf.sprintf "선언되지 않은 effect %s (%s의 effects 절은 %s입니다)"
|
||||||
|
(T.atom_show m) d.fn_name (T.eff_show declared_eff)))
|
||||||
|
missing)
|
||||||
|
(List.rev env.performed);
|
||||||
|
(* 과잉 선언도 오류다. 선언한 effect를 수행하지 않으면 호출자는 하지도
|
||||||
|
않는 일에 대한 의무를 진다 — 자기 effects 절을 넓히거나 capability를
|
||||||
|
받아오게 된다. 시그니처는 실제보다 좁아도 안 되고 넓어도 안 된다.
|
||||||
|
|
||||||
|
effect 변수가 있으면 판정하지 않는다. e에 무엇이 묶일지는 호출
|
||||||
|
지점이 정하고, 본문만 보고는 알 수 없다 — 모르는 것을 틀렸다고
|
||||||
|
말하지 않는다. *)
|
||||||
|
let has_var =
|
||||||
|
List.exists (function T.A_var _ -> true | _ -> false) declared_eff
|
||||||
|
in
|
||||||
|
if (not has_var) && not env.saw_unknown then
|
||||||
|
List.iter
|
||||||
|
(fun a ->
|
||||||
|
match a with
|
||||||
|
| T.A_name (cap, meth) ->
|
||||||
|
let performed = List.map fst env.performed |> T.eff_resolve in
|
||||||
|
if
|
||||||
|
not
|
||||||
|
(List.exists
|
||||||
|
(function
|
||||||
|
| T.A_name (c, m) -> c = cap && m = meth
|
||||||
|
| T.A_var _ | T.A_meta _ -> true)
|
||||||
|
performed)
|
||||||
|
then
|
||||||
|
err env d.fn_pos
|
||||||
|
(Printf.sprintf
|
||||||
|
"%s은(는) %s을(를) 선언했지만 수행하지 않습니다 (effects 절에서 지우십시오)"
|
||||||
|
d.fn_name (T.atom_show a))
|
||||||
|
| _ -> ())
|
||||||
|
declared_eff;
|
||||||
|
env.performed <- [];
|
||||||
pop env
|
pop env
|
||||||
|
|
||||||
let check (m : modul) : error list =
|
let check ?(imports : item list = []) (m : modul) : error list =
|
||||||
let env =
|
let env =
|
||||||
{
|
{
|
||||||
structs = Hashtbl.create 16;
|
structs = Hashtbl.create 16;
|
||||||
@@ -542,11 +806,46 @@ let check (m : modul) : error list =
|
|||||||
fns = Hashtbl.create 16;
|
fns = Hashtbl.create 16;
|
||||||
consts = Hashtbl.create 16;
|
consts = Hashtbl.create 16;
|
||||||
ctors = Hashtbl.create 16;
|
ctors = Hashtbl.create 16;
|
||||||
|
(* 표면을 실제로 받아온 별칭만 안다. 해소되지 않은 모듈(예: 아직 가져오지
|
||||||
|
못한 패키지)의 별칭은 모르는 것이므로 그 아래 이름을 틀렸다고 말하지
|
||||||
|
않는다. *)
|
||||||
|
aliases =
|
||||||
|
List.sort_uniq compare
|
||||||
|
(List.filter_map
|
||||||
|
(fun it ->
|
||||||
|
let n =
|
||||||
|
match it with
|
||||||
|
| I_fn { decl; _ } -> decl.fn_name
|
||||||
|
| I_struct { name; _ }
|
||||||
|
| I_enum { name; _ }
|
||||||
|
| I_capability { name; _ }
|
||||||
|
| I_const { name; _ } ->
|
||||||
|
name
|
||||||
|
| _ -> ""
|
||||||
|
in
|
||||||
|
match String.index_opt n '.' with
|
||||||
|
| Some i -> Some (String.sub n 0 i)
|
||||||
|
| None -> None)
|
||||||
|
imports);
|
||||||
locals = [];
|
locals = [];
|
||||||
ret = T.TUnit;
|
ret = T.TUnit;
|
||||||
|
performed = [];
|
||||||
|
saw_unknown = false;
|
||||||
errors = [];
|
errors = [];
|
||||||
}
|
}
|
||||||
in
|
in
|
||||||
|
(* 내장 열거형. 이것이 없으면 Option/Result의 패턴이 생성자로 인식되지
|
||||||
|
않고 변수 바인딩이 되어, match 안에서 타입 정보가 통째로 사라진다.
|
||||||
|
사용자 정의가 덮을 수 있도록 먼저 넣는다. *)
|
||||||
|
Hashtbl.replace env.enums "Option"
|
||||||
|
([ "a" ], [ ("Some", [ T.TVar "a" ]); ("None", []) ]);
|
||||||
|
Hashtbl.replace env.enums "Result"
|
||||||
|
([ "a"; "e" ], [ ("Ok", [ T.TVar "a" ]); ("Err", [ T.TVar "e" ]) ]);
|
||||||
|
List.iter
|
||||||
|
(fun (v, e) -> Hashtbl.replace env.ctors v e)
|
||||||
|
[
|
||||||
|
("Some", "Option"); ("None", "Option"); ("Ok", "Result"); ("Err", "Result");
|
||||||
|
];
|
||||||
(* 1차: 타입과 생성자 이름부터 (선언 순서에 의존하지 않는다) *)
|
(* 1차: 타입과 생성자 이름부터 (선언 순서에 의존하지 않는다) *)
|
||||||
List.iter
|
List.iter
|
||||||
(fun it ->
|
(fun it ->
|
||||||
@@ -559,7 +858,7 @@ let check (m : modul) : error list =
|
|||||||
List.iter (fun v -> Hashtbl.replace env.ctors v.v_name name) variants
|
List.iter (fun v -> Hashtbl.replace env.ctors v.v_name name) variants
|
||||||
| I_capability { name; _ } -> Hashtbl.replace env.caps name []
|
| I_capability { name; _ } -> Hashtbl.replace env.caps name []
|
||||||
| _ -> ())
|
| _ -> ())
|
||||||
m.items;
|
(imports @ m.items);
|
||||||
(* 2차: 본문을 채운다 *)
|
(* 2차: 본문을 채운다 *)
|
||||||
List.iter
|
List.iter
|
||||||
(fun it ->
|
(fun it ->
|
||||||
@@ -583,7 +882,7 @@ let check (m : modul) : error list =
|
|||||||
| I_const { name; ty; _ } ->
|
| I_const { name; ty; _ } ->
|
||||||
Hashtbl.replace env.consts name (conv env [] ty)
|
Hashtbl.replace env.consts name (conv env [] ty)
|
||||||
| _ -> ())
|
| _ -> ())
|
||||||
m.items;
|
(imports @ m.items);
|
||||||
(* 3차: 본문 검사 *)
|
(* 3차: 본문 검사 *)
|
||||||
List.iter
|
List.iter
|
||||||
(fun it ->
|
(fun it ->
|
||||||
@@ -594,6 +893,27 @@ let check (m : modul) : error list =
|
|||||||
let want = conv env [] ty in
|
let want = conv env [] ty in
|
||||||
let got = infer env value in
|
let got = infer env value in
|
||||||
if not (T.unify want got) then mismatch env pos want got "상수의 값"
|
if not (T.unify want got) then mismatch env pos want got "상수의 값"
|
||||||
|
(* 테스트는 파라미터 없고 effect 없는 함수와 같다. 검사도 같다 —
|
||||||
|
일반 코드와 다른 규칙을 주면 테스트만 통과하는 코드가 생긴다 *)
|
||||||
|
| I_test { name; body; pos } ->
|
||||||
|
env.locals <- [];
|
||||||
|
push env;
|
||||||
|
env.ret <- T.TUnit;
|
||||||
|
env.performed <- [];
|
||||||
|
env.saw_unknown <- false;
|
||||||
|
let got = infer_block env body in
|
||||||
|
if not (T.unify T.TUnit got) then
|
||||||
|
mismatch env pos T.TUnit got (Printf.sprintf "테스트 \"%s\"의 본문" name);
|
||||||
|
List.iter
|
||||||
|
(fun (a, apos) ->
|
||||||
|
err env apos
|
||||||
|
(Printf.sprintf
|
||||||
|
"테스트는 effect를 수행할 수 없습니다 (%s) — 테스트는 capability를 받지 않습니다"
|
||||||
|
(T.atom_show a)))
|
||||||
|
(List.rev (T.eff_resolve (List.map fst env.performed))
|
||||||
|
|> List.map (fun a -> (a, pos)));
|
||||||
|
env.performed <- [];
|
||||||
|
pop env
|
||||||
| _ -> ())
|
| _ -> ())
|
||||||
m.items;
|
m.items;
|
||||||
List.sort
|
List.sort
|
||||||
|
|||||||
+122
-18
@@ -1,37 +1,109 @@
|
|||||||
(* 타입 표현과 지역 unification.
|
(* 타입과 effect 표현, 그리고 지역 unification.
|
||||||
|
|
||||||
TUnknown이 핵심이다. 외부 모듈에서 오는 이름은 모듈 로딩이 없는 v0에서
|
TUnknown이 핵심이다. 외부 모듈에서 오는 이름은 모듈 로딩이 없는 v0에서
|
||||||
해소할 수 없다. 그런 타입은 TUnknown이 되고 무엇과도 맞는다 — 모르는 것을
|
해소할 수 없다. 그런 타입은 TUnknown이 되고 무엇과도 맞는다 — 모르는 것을
|
||||||
틀렸다고 말하지 않기 위해서다. 아는 범위에서만 검사한다.
|
틀렸다고 말하지 않기 위해서다.
|
||||||
|
|
||||||
TMeta는 호출 지점에서 제네릭을 인스턴스화할 때 생기는 미지수다. 함수 하나
|
effect는 순서 없는 집합이고 합성은 합집합이다. 변수는 집합 변수이며,
|
||||||
범위에서만 살고 전역으로 흐르지 않는다 (철학 2: 전역 추론 없음). *)
|
호출 지점에서 결정 위치(파라미터의 effect 자리에 단독으로 선 변수)를 통해
|
||||||
|
메타에 묶인다. 함수 하나 범위를 넘지 않는다. *)
|
||||||
|
|
||||||
type t =
|
type t =
|
||||||
| TUnknown
|
| TUnknown
|
||||||
|
(* 값을 내지 않는 타입. crash의 타입이고 어떤 자리에도 놓일 수 있다.
|
||||||
|
TUnknown과 다르다 — TUnknown은 "모른다"이고 TNever는 "돌아오지
|
||||||
|
않는다"이다. 둘 다 무엇과도 맞지만 생기는 이유가 다르다. *)
|
||||||
|
| TNever
|
||||||
| TInt
|
| TInt
|
||||||
| TBool
|
| TBool
|
||||||
| TString
|
| TString
|
||||||
| TUnit
|
| TUnit
|
||||||
| TVar of string
|
| TVar of string
|
||||||
| TCon of string * t list
|
| TCon of string * t list
|
||||||
| TFn of { affine : bool; params : t list; ret : t }
|
(* params의 bool은 소유권이다 (own이면 true). affinity와 다른 축이다 —
|
||||||
|
affinity는 타입의 성질이고 소유권은 이 자리가 값을 가져가는가다. *)
|
||||||
|
| TFn of { affine : bool; params : (bool * t) list; eff : eff; ret : t }
|
||||||
| TMeta of meta ref
|
| TMeta of meta ref
|
||||||
|
|
||||||
and meta = Unbound of int | Bound of t
|
and meta = Unbound of int | Bound of t
|
||||||
|
|
||||||
|
(* effect 집합. 원소는 구체 이름, 집합 변수, 또는 호출 지점의 미지수다. *)
|
||||||
|
and eff = atom list
|
||||||
|
|
||||||
|
and atom =
|
||||||
|
| A_name of string * string (* Cap.method *)
|
||||||
|
| A_var of string
|
||||||
|
| A_meta of emeta ref
|
||||||
|
|
||||||
|
and emeta = EUnbound of int | EBound of eff
|
||||||
|
|
||||||
let counter = ref 0
|
let counter = ref 0
|
||||||
|
|
||||||
let fresh () =
|
let fresh () =
|
||||||
incr counter;
|
incr counter;
|
||||||
TMeta (ref (Unbound !counter))
|
TMeta (ref (Unbound !counter))
|
||||||
|
|
||||||
|
let fresh_eff () =
|
||||||
|
incr counter;
|
||||||
|
[ A_meta (ref (EUnbound !counter)) ]
|
||||||
|
|
||||||
let rec resolve t =
|
let rec resolve t =
|
||||||
match t with TMeta { contents = Bound u } -> resolve u | _ -> t
|
match t with TMeta { contents = Bound u } -> resolve u | _ -> t
|
||||||
|
|
||||||
|
(* 묶인 메타를 펼치고 중복을 없앤다. 집합이므로 순서는 의미가 없다. *)
|
||||||
|
let rec eff_resolve (e : eff) : eff =
|
||||||
|
let expand a =
|
||||||
|
match a with
|
||||||
|
| A_meta { contents = EBound inner } -> eff_resolve inner
|
||||||
|
| _ -> [ a ]
|
||||||
|
in
|
||||||
|
let flat = List.concat_map expand e in
|
||||||
|
let mem a acc =
|
||||||
|
List.exists
|
||||||
|
(fun b ->
|
||||||
|
match (a, b) with
|
||||||
|
| A_name (c1, m1), A_name (c2, m2) -> c1 = c2 && m1 = m2
|
||||||
|
| A_var x, A_var y -> x = y
|
||||||
|
| A_meta r, A_meta r' -> r == r'
|
||||||
|
| _ -> false)
|
||||||
|
acc
|
||||||
|
in
|
||||||
|
List.fold_left (fun acc a -> if mem a acc then acc else a :: acc) [] flat
|
||||||
|
|> List.rev
|
||||||
|
|
||||||
|
let atom_show = function
|
||||||
|
| A_name (c, m) -> c ^ "." ^ m
|
||||||
|
| A_var v -> v
|
||||||
|
| A_meta { contents = EUnbound n } -> Printf.sprintf "_e%d" n
|
||||||
|
| A_meta { contents = EBound _ } -> "?"
|
||||||
|
|
||||||
|
let eff_show e =
|
||||||
|
match eff_resolve e with
|
||||||
|
| [] -> "{}"
|
||||||
|
| atoms -> "{" ^ String.concat ", " (List.map atom_show atoms) ^ "}"
|
||||||
|
|
||||||
|
let atom_eq a b =
|
||||||
|
match (a, b) with
|
||||||
|
| A_name (c1, m1), A_name (c2, m2) -> c1 = c2 && m1 = m2
|
||||||
|
| A_var x, A_var y -> x = y
|
||||||
|
| A_meta r, A_meta r' -> r == r'
|
||||||
|
| _ -> false
|
||||||
|
|
||||||
|
(* declared가 덮지 못하는 원소들. 미지수는 판정을 미룬다 —
|
||||||
|
결정되지 않은 것을 위반이라고 말하지 않는다. *)
|
||||||
|
let eff_missing ~declared ~performed =
|
||||||
|
let declared = eff_resolve declared in
|
||||||
|
List.filter
|
||||||
|
(fun a ->
|
||||||
|
match a with
|
||||||
|
| A_meta { contents = EUnbound _ } -> false
|
||||||
|
| _ -> not (List.exists (atom_eq a) declared))
|
||||||
|
(eff_resolve performed)
|
||||||
|
|
||||||
let rec show t =
|
let rec show t =
|
||||||
match resolve t with
|
match resolve t with
|
||||||
| TUnknown -> "?"
|
| TUnknown -> "?"
|
||||||
|
| TNever -> "Never"
|
||||||
| TInt -> "Int"
|
| TInt -> "Int"
|
||||||
| TBool -> "Bool"
|
| TBool -> "Bool"
|
||||||
| TString -> "String"
|
| TString -> "String"
|
||||||
@@ -39,10 +111,12 @@ let rec show t =
|
|||||||
| TVar v -> v
|
| TVar v -> v
|
||||||
| TCon (n, []) -> n
|
| TCon (n, []) -> n
|
||||||
| TCon (n, args) -> n ^ "[" ^ String.concat ", " (List.map show args) ^ "]"
|
| TCon (n, args) -> n ^ "[" ^ String.concat ", " (List.map show args) ^ "]"
|
||||||
| TFn { affine; params; ret } -> (
|
| TFn { affine; params; eff; ret } -> (
|
||||||
(if affine then "affine fn(" else "fn(")
|
(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)
|
^ match resolve ret with TUnit -> "" | r -> " -> " ^ show r)
|
||||||
| TMeta { contents = Unbound n } -> Printf.sprintf "_%d" n
|
| TMeta { contents = Unbound n } -> Printf.sprintf "_%d" n
|
||||||
| TMeta { contents = Bound _ } -> "?"
|
| TMeta { contents = Bound _ } -> "?"
|
||||||
@@ -51,14 +125,28 @@ let rec occurs r t =
|
|||||||
match resolve t with
|
match resolve t with
|
||||||
| TMeta r' -> r == r'
|
| TMeta r' -> r == r'
|
||||||
| TCon (_, args) -> List.exists (occurs r) args
|
| TCon (_, args) -> List.exists (occurs r) args
|
||||||
| TFn { params; ret; _ } -> List.exists (occurs r) params || occurs r ret
|
| TFn { params; ret; _ } ->
|
||||||
|
List.exists (fun (_, t) -> occurs r t) params || occurs r ret
|
||||||
| _ -> false
|
| _ -> false
|
||||||
|
|
||||||
(* 성공하면 true. 실패해도 예외를 던지지 않는다 — 호출자가 위치를 알고
|
(* 결정 위치의 해소: 파라미터의 effect 자리에 단독으로 선 미지수만 묶는다.
|
||||||
진단을 만든다. *)
|
그 외에는 참을 돌려주고, 실제 포함 검사는 호출 지점에서 방향을 아는
|
||||||
|
쪽이 한다 (진단 품질 때문에). *)
|
||||||
|
let unify_eff a b =
|
||||||
|
match (eff_resolve a, eff_resolve b) with
|
||||||
|
| [ A_meta ({ contents = EUnbound _ } as r) ], other ->
|
||||||
|
r := EBound other;
|
||||||
|
true
|
||||||
|
| other, [ A_meta ({ contents = EUnbound _ } as r) ] ->
|
||||||
|
r := EBound other;
|
||||||
|
true
|
||||||
|
| _ -> true
|
||||||
|
|
||||||
let rec unify a b =
|
let rec unify a b =
|
||||||
match (resolve a, resolve b) with
|
match (resolve a, resolve b) with
|
||||||
| TUnknown, _ | _, TUnknown -> true
|
| TUnknown, _ | _, TUnknown -> true
|
||||||
|
(* Never는 어떤 타입 자리에도 놓인다. crash가 match 팔에 설 수 있는 이유다 *)
|
||||||
|
| TNever, _ | _, TNever -> true
|
||||||
| TMeta r, TMeta r' when r == r' -> true
|
| TMeta r, TMeta r' when r == r' -> true
|
||||||
| TMeta r, t | t, TMeta r ->
|
| TMeta r, t | t, TMeta r ->
|
||||||
if occurs r t then false
|
if occurs r t then false
|
||||||
@@ -73,21 +161,37 @@ let rec unify a b =
|
|||||||
(* affinity는 타입 동등성의 일부가 아니다. 값이 affine인지는 무엇을
|
(* affinity는 타입 동등성의 일부가 아니다. 값이 affine인지는 무엇을
|
||||||
capture했는지로 정해지는 substructural 성질이고, move/affinity 검사가
|
capture했는지로 정해지는 substructural 성질이고, move/affinity 검사가
|
||||||
소유한다. 여기서 섞으면 두 검사가 서로의 결론을 앞질러 버린다. *)
|
소유한다. 여기서 섞으면 두 검사가 서로의 결론을 앞질러 버린다. *)
|
||||||
|
(* 소유권은 타입 동등성의 일부다. 빌리는 클로저를 소유 자리에 넘기는
|
||||||
|
것은 안전하지만 그 반대는 아니고, 방향을 다루려면 부분 타입이
|
||||||
|
필요하다. 우리에겐 없으므로 정확히 일치를 요구한다. *)
|
||||||
List.length f.params = List.length g.params
|
List.length f.params = List.length g.params
|
||||||
&& List.for_all2 unify f.params g.params
|
&& List.for_all2
|
||||||
&& unify f.ret g.ret
|
(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
|
| _ -> false
|
||||||
|
|
||||||
(* 제네릭 인스턴스화: TVar를 주어진 대입으로 바꾼다 *)
|
(* 제네릭 인스턴스화: 타입 변수와 effect 변수를 동시에 바꾼다 *)
|
||||||
let rec subst env t =
|
let rec subst tenv eenv t =
|
||||||
match resolve t with
|
match resolve t with
|
||||||
| TVar v -> ( match List.assoc_opt v env with Some u -> u | None -> TVar v)
|
| TVar v -> ( match List.assoc_opt v tenv with Some u -> u | None -> TVar v)
|
||||||
| TCon (n, args) -> TCon (n, List.map (subst env) args)
|
| TCon (n, args) -> TCon (n, List.map (subst tenv eenv) args)
|
||||||
| TFn f ->
|
| TFn f ->
|
||||||
TFn
|
TFn
|
||||||
{
|
{
|
||||||
affine = f.affine;
|
affine = f.affine;
|
||||||
params = List.map (subst env) f.params;
|
params = List.map (fun (o, t) -> (o, subst tenv eenv t)) f.params;
|
||||||
ret = subst env f.ret;
|
eff = subst_eff eenv f.eff;
|
||||||
|
ret = subst tenv eenv f.ret;
|
||||||
}
|
}
|
||||||
| u -> u
|
| u -> u
|
||||||
|
|
||||||
|
and subst_eff eenv (e : eff) : eff =
|
||||||
|
eff_resolve
|
||||||
|
(List.concat_map
|
||||||
|
(fun a ->
|
||||||
|
match a with
|
||||||
|
| A_var v -> (
|
||||||
|
match List.assoc_opt v eenv with Some s -> s | None -> [ a ])
|
||||||
|
| _ -> [ a ])
|
||||||
|
e)
|
||||||
|
|||||||
@@ -43,12 +43,15 @@ pub fn checkout(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 빌린 값을 다른 빌림 자리로 넘기는 것은 허용된다 (복제가 아니다).
|
// 빌린 값을 다른 빌림 자리로 넘기는 것은 허용된다 (복제가 아니다).
|
||||||
|
//
|
||||||
|
// each가 아니라 map인 이유: 결과를 버릴 방법이 언어에 없다. each는 값을
|
||||||
|
// 남기지 않는 클로저만 받으므로 Result를 삼킬 수 없고, ?는 클로저 밖으로
|
||||||
|
// 나가지 못한다. 실패를 못 본 척하려면 명시적으로 match해야 한다.
|
||||||
pub fn refund_all(
|
pub fn refund_all(
|
||||||
pay: PaymentGateway,
|
pay: PaymentGateway,
|
||||||
ids: List[OrderId],
|
ids: List[OrderId],
|
||||||
) effects {PaymentGateway.refund} -> Result[Unit, PayError] {
|
) effects {PaymentGateway.refund} -> List[Result[Receipt, PayError]] {
|
||||||
List.each(ids, fn(id) {
|
List.map(ids, fn(id) {
|
||||||
refund_order(pay, id)?
|
refund_order(pay, id)
|
||||||
Ok(unit)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
// 파라미터 위치의 effect 절은 `effects <변수>` 또는 `effects {리터럴}`만 가능하다.
|
// 파라미터 위치의 effect 절은 `effects <변수>` 또는 `effects {리터럴}`만 가능하다.
|
||||||
// 합집합은 결과 위치에서만 쓸 수 있다 — 문법이 그렇게 생겼다.
|
// 합집합은 결과 위치에서만 쓸 수 있다 — 문법이 그렇게 생겼다.
|
||||||
|
|
||||||
|
import "cool.dev/std/string" as String
|
||||||
|
|
||||||
pub fn map[a, b, e: effects](
|
pub fn map[a, b, e: effects](
|
||||||
xs: List[a],
|
xs: List[a],
|
||||||
f: fn(a) effects e -> b,
|
f: fn(a) effects e -> b,
|
||||||
|
|||||||
@@ -2,6 +2,19 @@
|
|||||||
//
|
//
|
||||||
// variant 추가가 downstream exhaustive match를 깨뜨리는 경로.
|
// variant 추가가 downstream exhaustive match를 깨뜨리는 경로.
|
||||||
|
|
||||||
|
import "cool.dev/std/string" as String
|
||||||
|
import "cool.dev/std/int" as Int
|
||||||
|
|
||||||
|
// 거절 코드. 예전에는 어디에도 없는 이름이었고 Code.show가 조용히
|
||||||
|
// 통과했다 — 이름 해소가 점 왼쪽을 검사하지 않아서다.
|
||||||
|
pub copyable struct Code {
|
||||||
|
value: Int,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_code(c: Code) -> String {
|
||||||
|
Int.show(c.value)
|
||||||
|
}
|
||||||
|
|
||||||
pub enum PayError {
|
pub enum PayError {
|
||||||
Declined(Code),
|
Declined(Code),
|
||||||
Network(Retryable),
|
Network(Retryable),
|
||||||
@@ -24,7 +37,7 @@ pub fn is_retryable(e: PayError) -> Bool {
|
|||||||
|
|
||||||
pub fn describe(e: PayError) -> String {
|
pub fn describe(e: PayError) -> String {
|
||||||
match e {
|
match e {
|
||||||
Declined(code) => String.concat("declined: ", Code.show(code)),
|
Declined(code) => String.concat("declined: ", show_code(code)),
|
||||||
Network(_) => "network",
|
Network(_) => "network",
|
||||||
Fraud => "fraud",
|
Fraud => "fraud",
|
||||||
}
|
}
|
||||||
|
|||||||
+77
-38
@@ -1,26 +1,38 @@
|
|||||||
// 05. 컴파일 에러가 나야 하는 코드
|
// 05. move / affinity 검사기가 거부해야 하는 코드
|
||||||
//
|
//
|
||||||
// 각 함수는 주석에 적힌 진단 하나를 정확히 내야 한다.
|
// 09, 10과 같은 이유로 외부 타입이 하나도 없다. affinity의 뿌리는 capability라
|
||||||
// 체커가 생기면 그대로 테스트 케이스가 된다.
|
// 자원 타입을 이 파일에서 정의해야 검사기가 affine임을 알 수 있다.
|
||||||
|
// 외부 타입은 affine임을 증명할 수 없으므로 copyable로 취급된다.
|
||||||
|
|
||||||
// close는 파일을 소비한다: own 유표기
|
pub capability File {
|
||||||
|
fn size() -> Int
|
||||||
|
}
|
||||||
|
|
||||||
|
pub capability Gateway {
|
||||||
|
fn refund(id: Int) effects {Gateway.refund}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub capability Registry {
|
||||||
|
fn add(h: fn()) effects {Registry.add}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파일을 소비하는 함수: own 유표기
|
||||||
pub fn close(own f: File) effects {File.close}
|
pub fn close(own f: File) effects {File.close}
|
||||||
|
|
||||||
// [E-move-after-move] affine 값의 이중 소비
|
// 빌리기만 하는 함수: 무표기
|
||||||
pub fn double_close(own f: File) effects {File.close} {
|
pub fn size_of(f: File) -> Int {
|
||||||
close(f)
|
f.size()
|
||||||
close(f) // ERROR: f는 이미 move됨 (앞줄에서 소비)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// [E-move-join] 분기 병합은 보수적 합집합
|
// --- 통과해야 하는 것 ---
|
||||||
pub fn conditional_close(own f: File, c: Bool) effects {File.close} {
|
|
||||||
if c {
|
pub fn use_then_close(own f: File) effects {File.close} -> Int {
|
||||||
|
let n = size_of(f)
|
||||||
close(f)
|
close(f)
|
||||||
}
|
n
|
||||||
close(f) // ERROR: f는 이 분기에서 move됨 (조건부 소비)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 정당한 형태 — 양쪽 분기에서 소비하면 통과해야 한다.
|
// 양쪽 분기에서 소비하면 통과한다
|
||||||
pub fn both_branches_close(own f: File, c: Bool) effects {File.close} {
|
pub fn both_branches_close(own f: File, c: Bool) effects {File.close} {
|
||||||
if c {
|
if c {
|
||||||
close(f)
|
close(f)
|
||||||
@@ -29,48 +41,75 @@ pub fn both_branches_close(own f: File, c: Bool) effects {File.close} {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 빌린 값을 다른 빌림 자리로 넘기는 것은 복제가 아니다
|
||||||
|
pub fn borrow_twice(f: File) -> Int {
|
||||||
|
size_of(f) + size_of(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// affine 값을 capture한 클로저는 affine fn이다
|
||||||
|
pub fn deferred_close(own f: File) -> affine fn() effects {File.close} {
|
||||||
|
fn() { close(f) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 여기서부터 전부 오류다 ---
|
||||||
|
|
||||||
|
// [E-move-after-move] affine 값의 이중 소비
|
||||||
|
pub fn double_close(own f: File) effects {File.close} {
|
||||||
|
close(f)
|
||||||
|
close(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// [E-move-join] 분기 병합은 보수적 합집합
|
||||||
|
pub fn conditional_close(own f: File, c: Bool) effects {File.close} {
|
||||||
|
if c {
|
||||||
|
close(f)
|
||||||
|
}
|
||||||
|
close(f)
|
||||||
|
}
|
||||||
|
|
||||||
// [E-use-escape] 빌린 값의 반환
|
// [E-use-escape] 빌린 값의 반환
|
||||||
pub fn leak_capability(pay: PaymentGateway) -> PaymentGateway {
|
pub fn leak_capability(pay: Gateway) -> Gateway {
|
||||||
pay // ERROR: 빌린 값은 반환할 수 없음 (own이 아니다)
|
pay
|
||||||
}
|
}
|
||||||
|
|
||||||
// [E-use-escape] 빌린 값의 저장
|
// [E-use-escape] 빌린 값의 저장
|
||||||
pub struct Holder {
|
pub struct Holder {
|
||||||
pay: PaymentGateway,
|
pay: Gateway,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store_capability(pay: PaymentGateway) -> Holder {
|
pub fn store_capability(pay: Gateway) -> Holder {
|
||||||
Holder { pay: pay } // ERROR: 빌린 값은 struct에 저장할 수 없음
|
Holder { pay: pay }
|
||||||
}
|
}
|
||||||
|
|
||||||
// [E-use-escape] 빌린 값을 capture한 클로저를 own 자리에 전달
|
// [E-use-escape] 빌린 값을 다른 함수에 소유로 넘긴다
|
||||||
pub fn register(own handler: fn()) effects {Registry.add}
|
pub fn give_away(f: File) effects {File.close} {
|
||||||
|
close(f)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn escape_via_closure(pay: PaymentGateway) effects {Registry.add} {
|
// [E-use-escape] 빌린 값을 capture한 클로저를 own 자리에 넘긴다
|
||||||
register(fn() { pay.refund(OrderId(1)) })
|
pub fn register(own h: fn() effects {Gateway.refund}) effects {Registry.add}
|
||||||
// ERROR: pay를 capture한 클로저는 빌린 값이며 own 자리에 전달할 수 없음
|
|
||||||
|
pub fn escape_via_closure(pay: Gateway) effects {Registry.add} {
|
||||||
|
register(fn() { pay.refund(1) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// [E-affinity-transitive] affine 필드를 가진 타입을 copyable로 선언
|
// [E-affinity-transitive] affine 필드를 가진 타입을 copyable로 선언
|
||||||
pub copyable struct Box {
|
pub copyable struct Box {
|
||||||
f: File, // ERROR: affine 필드(File)와 copyable 선언은 공존할 수 없음
|
f: File,
|
||||||
}
|
}
|
||||||
|
|
||||||
// [E-callable-affinity] affine 값을 capture한 클로저를 fn 위치에 대입
|
// [E-callable-affinity] affine 값을 capture한 클로저를 fn 위치에 반환
|
||||||
pub fn misuse_affine_closure(own f: File) -> fn() effects {File.close} {
|
pub fn misuse_affine_closure(own f: File) -> fn() effects {File.close} {
|
||||||
fn() { close(f) }
|
fn() { close(f) }
|
||||||
// ERROR: f를 capture했으므로 타입은 affine fn()이며 fn() 위치에 대입할 수 없음
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// [E-spawn-capture] spawn 클로저의 mutable capture
|
// [E-closure-mut-capture] 클로저는 mut 바인딩을 capture할 수 없다
|
||||||
pub fn spawn_mutable(sc: TaskScope, mut counter: Int) effects {TaskScope.spawn} {
|
pub fn capture_mut(own f: File, pay: Gateway)
|
||||||
scope s = sc {
|
effects {File.close, Registry.add, Gateway.refund} {
|
||||||
sc.spawn(fn() { counter = counter + 1 })
|
let mut counter = 0
|
||||||
// ERROR: spawn 클로저는 mutable 참조를 capture할 수 없음
|
register(fn() {
|
||||||
}
|
counter = counter + 1
|
||||||
}
|
pay.refund(counter)
|
||||||
|
})
|
||||||
// [E-effect-undeclared] 선언되지 않은 effect
|
close(f)
|
||||||
pub fn silent_write(log: Logger) {
|
|
||||||
log.write("hi") // ERROR: effect Logger.write가 시그니처에 선언되지 않음
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
//
|
//
|
||||||
// affinity는 타입 속성, own/무표기는 파라미터 위치 속성. 둘은 직교한다.
|
// affinity는 타입 속성, own/무표기는 파라미터 위치 속성. 둘은 직교한다.
|
||||||
|
|
||||||
|
import "cool.dev/std/string" as String
|
||||||
|
|
||||||
|
// 예전에는 File이 어디에도 없는 이름이었고 File.close(f)가 조용히
|
||||||
|
// 통과했다. 이제 선언한다 — 그리고 own self가 있으므로 닫는 일은
|
||||||
|
// 핸들 자신의 메서드다.
|
||||||
|
pub capability File {
|
||||||
|
fn close(own self) effects {File.close}
|
||||||
|
}
|
||||||
|
|
||||||
// copyable 환경만 capture → fn. 복사해서 여러 번 써도 된다.
|
// copyable 환경만 capture → fn. 복사해서 여러 번 써도 된다.
|
||||||
pub fn make_formatter(prefix: String) -> fn(String) -> String {
|
pub fn make_formatter(prefix: String) -> fn(String) -> String {
|
||||||
fn(s) { String.concat(prefix, s) }
|
fn(s) { String.concat(prefix, s) }
|
||||||
@@ -9,7 +18,7 @@ pub fn make_formatter(prefix: String) -> fn(String) -> String {
|
|||||||
|
|
||||||
// affine 값을 소유해 가므로 own. 결과 클로저는 자동으로 affine fn이 된다.
|
// affine 값을 소유해 가므로 own. 결과 클로저는 자동으로 affine fn이 된다.
|
||||||
pub fn deferred_close(own f: File) -> affine fn() effects {File.close} {
|
pub fn deferred_close(own f: File) -> affine fn() effects {File.close} {
|
||||||
fn() { File.close(f) }
|
fn() { f.close() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// affine fn을 호출 후 소비하려면 소유해야 한다.
|
// affine fn을 호출 후 소비하려면 소유해야 한다.
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
//
|
//
|
||||||
// 05와 목적이 다르다. 05는 구문은 맞지만 검사기가 거부해야 하는 파일이고,
|
// 05와 목적이 다르다. 05는 구문은 맞지만 검사기가 거부해야 하는 파일이고,
|
||||||
// 이 파일은 파서가 거부해야 하는 파일이다.
|
// 이 파일은 파서가 거부해야 하는 파일이다.
|
||||||
// 아직 오류 복구가 없으므로 파서는 첫 오류에서 멈춘다 — 한 번에 하나씩 확인한다.
|
// 파서는 항목 단위로 회복한다. 오류가 난 선언은 통째로 버리고 다음 선언에서
|
||||||
|
// 다시 시작하므로, 한 항목에 오류 하나가 상한이다. 이 파일은 항목마다 하나씩
|
||||||
|
// 심어 회복이 실제로 되는지 본다 — 아래 넷이 모두 보고되어야 한다.
|
||||||
|
|
||||||
// [E-syntax-effect-union] 파라미터 위치의 합집합은 문법에 존재하지 않는다.
|
// [E-syntax-effect-union] 파라미터 위치의 합집합은 문법에 존재하지 않는다.
|
||||||
// 검사기가 아니라 파서가 거부한다 (eff_param 프로덕션에 "|"가 없다).
|
// 검사기가 아니라 파서가 거부한다 (eff_param 프로덕션에 "|"가 없다).
|
||||||
@@ -19,3 +21,25 @@ pub fn classify(e: PayError) -> String {
|
|||||||
_ => "other",
|
_ => "other",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [E-syntax-trailing-comma] 다중 줄 목록에는 후행 콤마가 필요하다.
|
||||||
|
// 줄바꿈이 목록을 닫으려 하면 원인을 직접 말한다.
|
||||||
|
pub fn missing_comma(
|
||||||
|
a: Int,
|
||||||
|
b: Int
|
||||||
|
) -> Int
|
||||||
|
// ERROR (parse): 다중 줄 목록에는 후행 콤마가 필요합니다
|
||||||
|
|
||||||
|
// [E-syntax-scope-parent] 자식 scope는 부모를 명시해야 한다.
|
||||||
|
// 부모 없는 작업이 생기지 않게 하는 것은 문법의 일이다.
|
||||||
|
pub fn orphan(root: TaskScope) {
|
||||||
|
scope sc {
|
||||||
|
sc.spawn(fn() { })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ERROR (parse): = (자식 scope의 부모를 명시해야 합니다)
|
||||||
|
|
||||||
|
// 회복이 되었다는 증거: 이 마지막 선언은 정상적으로 읽혀야 한다.
|
||||||
|
pub fn fine(n: Int) -> Int {
|
||||||
|
n + 1
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
// 10. effect 검사기가 거부해야 하는 코드
|
||||||
|
//
|
||||||
|
// 09와 같은 이유로 외부 타입이 하나도 없다. capability를 이 파일에서 정의해야
|
||||||
|
// 메서드의 effect가 알려지고, 검사기가 실제로 판정할 수 있다.
|
||||||
|
|
||||||
|
pub capability Db {
|
||||||
|
fn read(id: Int) effects {Db.read} -> Int
|
||||||
|
fn write(id: Int, v: Int) effects {Db.write}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub capability Log {
|
||||||
|
fn write(msg: String) effects {Log.write}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 통과해야 하는 것 ---
|
||||||
|
|
||||||
|
pub fn get(db: Db, id: Int) effects {Db.read} -> Int {
|
||||||
|
db.read(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy(db: Db, from: Int, to: Int) effects {Db.read, Db.write} {
|
||||||
|
db.write(to, db.read(from))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 헬퍼를 부르면 헬퍼의 effect를 물려받는다
|
||||||
|
pub fn get_twice(db: Db, id: Int) effects {Db.read} -> Int {
|
||||||
|
get(db, id) + get(db, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// effect 변수: 결정 위치의 변수가 인자의 effect로 묶인다
|
||||||
|
pub fn twice[e: effects](f: fn() effects e) effects e {
|
||||||
|
f()
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_twice(log: Log) effects {Log.write} {
|
||||||
|
twice(fn() { log.write("hi") })
|
||||||
|
}
|
||||||
|
|
||||||
|
// effect 없는 함수는 effects 절이 없다
|
||||||
|
pub fn pure_add(a: Int, b: Int) -> Int {
|
||||||
|
a + b
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 여기서부터 전부 오류다 ---
|
||||||
|
|
||||||
|
// [E-effect-undeclared] 선언 없이 capability 메서드를 부른다
|
||||||
|
pub fn silent_read(db: Db) -> Int {
|
||||||
|
db.read(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// [E-effect-undeclared] 일부만 선언했다
|
||||||
|
pub fn partial(db: Db, id: Int) effects {Db.read} {
|
||||||
|
db.write(id, db.read(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// [E-effect-undeclared] 헬퍼가 수행하는 effect도 물려받아야 한다
|
||||||
|
pub fn via_helper(db: Db, id: Int) -> Int {
|
||||||
|
get(db, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// [E-effect-undeclared] 클로저를 통해 새어 나오는 effect
|
||||||
|
pub fn via_closure(log: Log) {
|
||||||
|
twice(fn() { log.write("hi") })
|
||||||
|
}
|
||||||
|
|
||||||
|
// [E-effect-closure-annotated] 클로저가 선언한 것보다 많이 수행한다
|
||||||
|
pub fn closure_lies(log: Log) effects {Log.write} {
|
||||||
|
twice(fn() effects {} { log.write("hi") })
|
||||||
|
}
|
||||||
|
|
||||||
|
// [E-effect-param] 파라미터가 허용한 effect를 넘는 함수를 넘긴다
|
||||||
|
pub fn takes_pure(f: fn() effects {}) {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pass_impure(log: Log) effects {Log.write} {
|
||||||
|
takes_pure(fn() { log.write("hi") })
|
||||||
|
}
|
||||||
|
|
||||||
|
// [E-capability-static] capability 메서드를 타입 이름으로 부른다.
|
||||||
|
// 이것이 허용되면 capability 없이 effect를 수행할 수 있게 되어
|
||||||
|
// "capability 없이는 effect를 수행할 수 없다"는 정리가 무너진다.
|
||||||
|
pub fn no_instance() effects {Db.read} -> Int {
|
||||||
|
Db.read(1)
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
// 11. exhaustiveness 검사기가 거부해야 하는 코드
|
||||||
|
//
|
||||||
|
// 철학 1의 대표 항목이자, interface hash가 enum 정의 본문을 입력으로 삼는 이유다.
|
||||||
|
// upstream에 variant가 하나 늘면 downstream의 match가 깨져야 하는데,
|
||||||
|
// 이 검사가 없으면 깨질 것이 없다.
|
||||||
|
|
||||||
|
pub enum Shape {
|
||||||
|
Circle(Int),
|
||||||
|
Rect(Int, Int),
|
||||||
|
Point,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 통과해야 하는 것 ---
|
||||||
|
|
||||||
|
pub fn area(s: Shape) -> Int {
|
||||||
|
match s {
|
||||||
|
Circle(r) => r * r,
|
||||||
|
Rect(w, h) => w * h,
|
||||||
|
Point => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_wildcard(s: Shape) -> Int {
|
||||||
|
match s {
|
||||||
|
Circle(r) => r,
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nested_full(o: Option[Shape]) -> Int {
|
||||||
|
match o {
|
||||||
|
Some(Circle(r)) => r,
|
||||||
|
Some(Rect(w, h)) => w * h,
|
||||||
|
Some(Point) => 0,
|
||||||
|
None => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn results(r: Result[Int, Int]) -> Int {
|
||||||
|
match r {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flags(b: Bool) -> Int {
|
||||||
|
match b {
|
||||||
|
true => 1,
|
||||||
|
false => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 여기서부터 전부 오류다 ---
|
||||||
|
|
||||||
|
// [E-match-missing] variant 하나가 빠졌다
|
||||||
|
pub fn missing_variant(s: Shape) -> Int {
|
||||||
|
match s {
|
||||||
|
Circle(r) => r,
|
||||||
|
Rect(w, h) => w * h,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [E-match-missing] Bool도 생성자 집합이 유한하다
|
||||||
|
pub fn missing_false(b: Bool) -> Int {
|
||||||
|
match b {
|
||||||
|
true => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [E-match-missing] Option
|
||||||
|
pub fn missing_none(o: Option[Int]) -> Int {
|
||||||
|
match o {
|
||||||
|
Some(n) => n,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [E-match-missing] 중첩된 자리에서 빠진 경우도 찾는다
|
||||||
|
pub fn missing_nested(o: Option[Shape]) -> Int {
|
||||||
|
match o {
|
||||||
|
Some(Circle(r)) => r,
|
||||||
|
None => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [E-match-missing] Int 리터럴은 생성자 집합이 무한하다
|
||||||
|
pub fn missing_literal(n: Int) -> Int {
|
||||||
|
match n {
|
||||||
|
0 => 1,
|
||||||
|
1 => 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [E-match-unreachable] 앞의 와일드카드에 가린다
|
||||||
|
pub fn shadowed(s: Shape) -> Int {
|
||||||
|
match s {
|
||||||
|
_ => 0,
|
||||||
|
Point => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [E-match-unreachable] 같은 생성자가 두 번
|
||||||
|
pub fn duplicated(s: Shape) -> Int {
|
||||||
|
match s {
|
||||||
|
Circle(r) => r,
|
||||||
|
Circle(x) => x,
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// 12. 표준 라이브러리 시그니처가 실제로 검사된다
|
||||||
|
//
|
||||||
|
// std가 생기기 전에는 List.each가 모르는 이름이라 조용히 통과했다.
|
||||||
|
// "모르는 것을 틀렸다고 말하지 않는다"는 맞는 원칙이지만, 그 그늘에
|
||||||
|
// 검사되지 않는 영역이 숨어 있었다. 이제 그늘이 없다.
|
||||||
|
//
|
||||||
|
// 이 파일은 세 가지 오류를 낸다.
|
||||||
|
|
||||||
|
import "cool.dev/std/list" as List
|
||||||
|
import "cool.dev/std/int" as Int
|
||||||
|
|
||||||
|
pub capability Console {
|
||||||
|
fn print(s: String) effects {Console.print}
|
||||||
|
}
|
||||||
|
|
||||||
|
// (1) effect 다형성. e는 클로저의 시그니처에서 {Console.print}로 묶이고,
|
||||||
|
// 그것이 이 함수의 effects 절 {}를 넘는다. effect 변수가 호출 지점에서
|
||||||
|
// 실제로 해소된다는 증거다 — 이게 안 걸리면 규칙이 장식이다.
|
||||||
|
pub fn leaks_effect(c: Console, xs: List[Int]) {
|
||||||
|
List.each(xs, fn(n) {
|
||||||
|
c.print(Int.show(n))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// (2) 인자 개수
|
||||||
|
pub fn wrong_arity(xs: List[Int]) -> Int {
|
||||||
|
List.len(xs, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// (3) 반환 타입
|
||||||
|
pub fn wrong_type(xs: List[Int]) -> String {
|
||||||
|
List.len(xs)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// 13. 두 lint — 미사용 import와 effect 과잉 선언
|
||||||
|
//
|
||||||
|
// 둘 다 취향 문제가 아니라 비용 문제다.
|
||||||
|
//
|
||||||
|
// 미사용 import: 쓰지도 않는 모듈의 시그니처가 바뀌면 이 모듈이 재검사된다.
|
||||||
|
// 증분 루프의 비용을 아무 이유 없이 넓히는 선언이다.
|
||||||
|
//
|
||||||
|
// effect 과잉 선언: 선언한 effect를 수행하지 않으면 호출자는 하지도 않는
|
||||||
|
// 일에 대한 의무를 진다 — 자기 effects 절을 넓히거나 capability를 받아온다.
|
||||||
|
// 시그니처는 실제보다 좁아도 안 되고 넓어도 안 된다.
|
||||||
|
|
||||||
|
import "cool.dev/std/list" as List
|
||||||
|
// ERROR: List을(를) 가져왔지만 쓰지 않습니다
|
||||||
|
|
||||||
|
pub capability Db {
|
||||||
|
fn read(id: Int) effects {Db.read} -> Int
|
||||||
|
fn write(id: Int) effects {Db.write}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Db.write는 선언만 하고 수행하지 않는다.
|
||||||
|
pub fn only_reads(db: Db, id: Int) effects {Db.read, Db.write} -> Int {
|
||||||
|
db.read(id)
|
||||||
|
}
|
||||||
|
// ERROR: only_reads은(는) Db.write을(를) 선언했지만 수행하지 않습니다
|
||||||
|
|
||||||
|
// 정확히 선언하면 통과한다.
|
||||||
|
pub fn honest(db: Db, id: Int) effects {Db.read} -> Int {
|
||||||
|
db.read(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// effect 변수가 있으면 판정하지 않는다. e에 무엇이 묶일지는 호출 지점이
|
||||||
|
// 정하고, 본문만 보고는 알 수 없다 — 모르는 것을 틀렸다고 말하지 않는다.
|
||||||
|
pub fn polymorphic[e: effects](f: fn() effects e) effects e {
|
||||||
|
f()
|
||||||
|
}
|
||||||
+32
-7
@@ -12,19 +12,44 @@
|
|||||||
| 02_higher_order_effects | effect 변수, 구문 수준 제한, 명시적 인스턴스화 |
|
| 02_higher_order_effects | effect 변수, 구문 수준 제한, 명시적 인스턴스화 |
|
||||||
| 03_scope_concurrency | TaskScope, 이름 있는 scope, 중첩 시 수명 표현 |
|
| 03_scope_concurrency | TaskScope, 이름 있는 scope, 중첩 시 수명 표현 |
|
||||||
| 04_enum_match_interface | enum 정의 본문이 interface surface에 들어가는 경로 |
|
| 04_enum_match_interface | enum 정의 본문이 interface surface에 들어가는 경로 |
|
||||||
| 05_move_errors | **에러가 나야 하는** 코드 — 진단 하나씩 |
|
| 05_move_errors | **move/affinity 검사기가** 거부해야 하는 코드 (자원을 직접 정의) |
|
||||||
| 06_affine_closure | callable affinity (fn vs affine fn), own과의 직교성 |
|
| 06_affine_closure | callable affinity (fn vs affine fn), own과의 직교성 |
|
||||||
| 07_module_interface | interface artifact가 담아야 할 것 전부 |
|
| 07_module_interface | interface artifact가 담아야 할 것 전부 |
|
||||||
| 08_syntax_errors | **파서가** 거부해야 하는 코드 |
|
| 08_syntax_errors | **파서가** 거부해야 하는 코드 |
|
||||||
| 09_type_errors | **타입 검사기가** 거부해야 하는 코드 (외부 타입 0개) |
|
| 09_type_errors | **타입 검사기가** 거부해야 하는 코드 (외부 타입 0개) |
|
||||||
|
| 10_effect_errors | **effect 검사기가** 거부해야 하는 코드 (capability를 직접 정의) |
|
||||||
|
| 11_exhaustiveness | **exhaustiveness 검사기가** 거부해야 하는 코드 |
|
||||||
|
|
||||||
05, 08, 09는 통과하면 안 되는 파일이다. 각 함수 주석의 `[E-...]` 태그가 기대
|
05, 08, 09, 10은 통과하면 안 되는 파일이다. 각 함수 주석의 `[E-...]` 태그가
|
||||||
진단이며, 셋의 목적이 다르다 — **08은 파서가, 09는 타입 검사기가, 05는 아직
|
기대 진단이며, 넷의 목적이 다르다 — **08은 파서가, 09는 타입 검사기가,
|
||||||
없는 move/affinity 검사가** 거부해야 한다. 단계별로 파일을 나눈 이유는
|
10은 effect 검사기가, 05는 move/affinity 검사가** 거부해야 한다. 단계별로
|
||||||
앞 단계가 첫 오류에서 멈추면 뒤 단계 케이스에 영영 도달하지 못하기 때문이다.
|
파일을 나눈 이유는 앞 단계가 첫 오류에서 멈추면 뒤 단계 케이스에 영영
|
||||||
|
도달하지 못하기 때문이다.
|
||||||
|
|
||||||
09에는 외부 타입이 하나도 없다. 전부 모듈 안에서 정의되므로 검사기가
|
05, 09, 10에는 외부 타입이 하나도 없다. 전부 모듈 안에서 정의되므로 검사기가
|
||||||
TUnknown으로 빠져나갈 구석이 없다 — 검사기에 이빨이 있는지 보는 파일이다.
|
빠져나갈 구석이 없다 — 검사기에 이빨이 있는지 보는 파일들이다. 10은
|
||||||
|
capability를 직접 정의해야 메서드의 effect가 알려지고, 05는 affinity의 뿌리가
|
||||||
|
capability라 자원 타입을 정의해야 affine임이 유도된다.
|
||||||
|
|
||||||
|
13은 두 lint다. 미사용 import는 재검사 범위를 넓히고, effect 과잉 선언은
|
||||||
|
호출자에게 없는 의무를 지운다 — 둘 다 취향이 아니라 비용이다. 미사용
|
||||||
|
import는 lint이므로 뒤 단계를 막지 않는다: 같은 파일의 타입 오류가 함께
|
||||||
|
보고된다.
|
||||||
|
|
||||||
|
12는 표준 라이브러리가 생긴 뒤에야 가능해진 파일이다. std가 없을 때는
|
||||||
|
`List.each`가 모르는 이름이라 조용히 통과했다 — "모르는 것을 틀렸다고 말하지
|
||||||
|
않는다"는 맞는 원칙이지만 그 그늘에 검사되지 않는 영역이 있었다.
|
||||||
|
|
||||||
|
01~04, 06, 07은 `coolc check`를 통과한다 (exit 0).
|
||||||
|
|
||||||
|
`modules/`는 모듈 경계다. `coolc check modules/area.cool`이 import를 따라
|
||||||
|
shapes를 먼저 검사하고, `coolc iface modules/shapes.cool`이 downstream이
|
||||||
|
보는 표면과 그 해시를 보여준다.
|
||||||
|
|
||||||
|
`run/`은 실제로 돈다. `coolc run run/hello.cool`. 이 파일들은 검사를
|
||||||
|
통과한다가 아니라 무엇을 출력하는지까지 말한다 — 기대 출력이 주석에 있고
|
||||||
|
같은 것을 test/가 검사한다. main이 선언한 capability만 런타임이 넘기므로,
|
||||||
|
파라미터에서 Console을 지우면 출력할 방법이 프로그램 안에 없다.
|
||||||
|
|
||||||
파서는 첫 오류에서 멈춘다(오류 복구 미구현). 타입 검사기는 오류를 전부 모은다.
|
파서는 첫 오류에서 멈춘다(오류 복구 미구현). 타입 검사기는 오류를 전부 모은다.
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// 모듈 B. A의 표면에만 의존한다.
|
||||||
|
//
|
||||||
|
// Shape의 variant가 늘면 이 match가 깨진다 — 그래서 enum 정의 본문이
|
||||||
|
// interface hash 입력이고, A의 시그니처 변경은 여기까지 전파되어야 한다.
|
||||||
|
|
||||||
|
import "shapes" as Shapes
|
||||||
|
|
||||||
|
pub fn area(s: Shapes.Shape) -> Int {
|
||||||
|
match s {
|
||||||
|
Shapes.Circle(r) => Shapes.double(r),
|
||||||
|
Shapes.Square(w) => w * w,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 모듈 A. downstream이 보는 것은 이 파일의 exported surface뿐이다.
|
||||||
|
|
||||||
|
pub enum Shape {
|
||||||
|
Circle(Int),
|
||||||
|
Square(Int),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn double(n: Int) -> Int {
|
||||||
|
// 본문. 이 안을 아무리 고쳐도 interface hash는 변하지 않는다.
|
||||||
|
n + n
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// 실행 의미를 한 파일에 모은 것: ?, mut, struct, match, scope.
|
||||||
|
//
|
||||||
|
// 기대 출력:
|
||||||
|
// 7
|
||||||
|
// 6
|
||||||
|
// err
|
||||||
|
// in scope
|
||||||
|
|
||||||
|
import "cool.dev/std/int" as Int
|
||||||
|
|
||||||
|
pub capability Console {
|
||||||
|
fn print(s: String) effects {Console.print}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum E {
|
||||||
|
Bad,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub copyable struct P {
|
||||||
|
x: Int,
|
||||||
|
y: Int,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn half(n: Int) -> Result[Int, E] {
|
||||||
|
if n % 2 == 0 {
|
||||||
|
Ok(n / 2)
|
||||||
|
} else {
|
||||||
|
Err(Bad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn twice(n: Int) -> Result[Int, E] {
|
||||||
|
let a = half(n)?
|
||||||
|
let b = half(a)?
|
||||||
|
Ok(a + b)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_res(r: Result[Int, E]) -> String {
|
||||||
|
match r {
|
||||||
|
Ok(v) => Int.show(v),
|
||||||
|
Err(_) => "err",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(c: Console, root: TaskScope) effects {Console.print} {
|
||||||
|
let mut total = 0
|
||||||
|
let p = P { x: 3, y: 4 }
|
||||||
|
total = total + p.x + p.y
|
||||||
|
c.print(Int.show(total))
|
||||||
|
c.print(show_res(twice(8)))
|
||||||
|
c.print(show_res(twice(7)))
|
||||||
|
scope sc = root {
|
||||||
|
sc.spawn(fn() {
|
||||||
|
c.print("in scope")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// 실행되는 첫 프로그램.
|
||||||
|
//
|
||||||
|
// main은 자기가 선언한 capability만 받는다. Console을 파라미터에서 지우면
|
||||||
|
// print할 방법이 프로그램 안에 없다 — ambient authority가 없다는 것의
|
||||||
|
// 실행 시점 의미다.
|
||||||
|
//
|
||||||
|
// 표준 라이브러리도 명시적으로 가져온다. prelude가 없다 —
|
||||||
|
// 암묵적으로 끌어오지 않는다는 규칙에 예외를 두지 않는다.
|
||||||
|
//
|
||||||
|
// 기대 출력:
|
||||||
|
// area = 12
|
||||||
|
// area = 9
|
||||||
|
// area = 3
|
||||||
|
|
||||||
|
import "cool.dev/std/list" as List
|
||||||
|
import "cool.dev/std/string" as String
|
||||||
|
import "cool.dev/std/int" as Int
|
||||||
|
|
||||||
|
pub capability Console {
|
||||||
|
fn print(s: String) effects {Console.print}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Shape {
|
||||||
|
Circle(Int),
|
||||||
|
Square(Int),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn area(s: Shape) -> Int {
|
||||||
|
match s {
|
||||||
|
Circle(r) => r * r * 3,
|
||||||
|
Square(w) => w * w,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(c: Console) effects {Console.print} {
|
||||||
|
let shapes = [Circle(2), Square(3), Circle(1)]
|
||||||
|
List.each(shapes, fn(s) {
|
||||||
|
c.print(String.concat("area = ", Int.show(area(s))))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// 표준 라이브러리: 불리언.
|
||||||
|
|
||||||
|
pub fn show(b: Bool) -> String
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// 표준 라이브러리: 정수.
|
||||||
|
|
||||||
|
pub fn show(n: Int) -> String
|
||||||
|
pub fn abs(n: Int) -> Int
|
||||||
|
|
||||||
|
// 실패할 수 있으므로 Result다. 예외도, 0을 돌려주는 관례도 없다.
|
||||||
|
pub fn parse(s: String) -> Result[Int, String]
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// 표준 라이브러리: 리스트.
|
||||||
|
//
|
||||||
|
// 본문이 없다. 런타임이 구현하고, 이 파일은 그 계약을 말한다.
|
||||||
|
// 그래서 이 파일은 구현이 아니라 시험대다 — effect 다형성이 실제로 쓸 만한지가
|
||||||
|
// each, map, fold, filter에서 결정된다. 규칙이 틀렸으면 여기서 드러난다.
|
||||||
|
//
|
||||||
|
// e는 파라미터의 effect 슬롯에 홀로 나타난다 (결정 위치). 호출 지점에서
|
||||||
|
// 인자의 시그니처를 읽어 묶인다 — 추론이 아니라 읽기다.
|
||||||
|
|
||||||
|
pub fn len[a](xs: List[a]) -> Int
|
||||||
|
|
||||||
|
pub fn is_empty[a](xs: List[a]) -> Bool
|
||||||
|
|
||||||
|
// 인덱싱 연산자가 언어에 없다. n번째를 꺼내는 일은 이름 있는 함수가 하고,
|
||||||
|
// 없을 수 있다는 사실은 Option이 말한다 — 범위를 벗어나면 예외도 기본값도
|
||||||
|
// 아니고 None이다.
|
||||||
|
pub fn first[a](xs: List[a]) -> Option[a]
|
||||||
|
|
||||||
|
pub fn nth[a](xs: List[a], i: Int) -> Option[a]
|
||||||
|
|
||||||
|
// 첫 원소를 뺀 나머지. 비어 있으면 빈 리스트다 — 없는 것을 빼는 일은
|
||||||
|
// 오류가 아니라 아무 일도 아니다. first와 짝이고, 둘은 서로 어긋날 수 없다.
|
||||||
|
pub fn rest[a](xs: List[a]) -> List[a]
|
||||||
|
|
||||||
|
// 번호를 붙인 값. fold에 인덱스를 넣는 대신 이것 하나를 둔다 —
|
||||||
|
// fold_indexed를 만들면 map_indexed, each_indexed가 따라오고 그것이
|
||||||
|
// "한 개념 한 방식"을 깨는 방향이다. enumerate는 기존 각 함수와 조합된다.
|
||||||
|
pub copyable struct Indexed[a] {
|
||||||
|
i: Int,
|
||||||
|
value: a,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enumerate[a](xs: List[a]) -> List[Indexed[a]]
|
||||||
|
|
||||||
|
// 뒤에 하나 붙인 새 리스트. 제자리 수정이 아니다.
|
||||||
|
pub fn push[a](xs: List[a], x: a) -> List[a]
|
||||||
|
|
||||||
|
pub fn concat[a](xs: List[a], ys: List[a]) -> List[a]
|
||||||
|
|
||||||
|
pub fn reverse[a](xs: List[a]) -> List[a]
|
||||||
|
|
||||||
|
pub fn each[a, e: effects](
|
||||||
|
xs: List[a],
|
||||||
|
f: fn(a) effects e,
|
||||||
|
) effects e
|
||||||
|
|
||||||
|
pub fn map[a, b, e: effects](
|
||||||
|
xs: List[a],
|
||||||
|
f: fn(a) effects e -> b,
|
||||||
|
) effects e -> List[b]
|
||||||
|
|
||||||
|
pub fn filter[a, e: effects](
|
||||||
|
xs: List[a],
|
||||||
|
keep: fn(a) effects e -> Bool,
|
||||||
|
) effects e -> List[a]
|
||||||
|
|
||||||
|
// 누적자는 매 단계 소비되고 새것으로 바뀐다. own이 그 사실을 말한다 —
|
||||||
|
// 빌림으로 적으면 affine 값을 fold로 실어나를 수 없다.
|
||||||
|
pub fn fold[a, acc, e: effects](
|
||||||
|
xs: List[a],
|
||||||
|
own init: acc,
|
||||||
|
f: fn(own acc, a) effects e -> acc,
|
||||||
|
) effects e -> acc
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// 표준 라이브러리: 문자열.
|
||||||
|
//
|
||||||
|
// 인덱싱 연산자가 언어에 없다. 문자열을 자르는 일은 이름 있는 함수가 한다 —
|
||||||
|
// 한 개념 한 방식.
|
||||||
|
|
||||||
|
pub fn len(s: String) -> Int
|
||||||
|
pub fn is_empty(s: String) -> Bool
|
||||||
|
pub fn concat(a: String, b: String) -> String
|
||||||
|
|
||||||
|
// 구분자로 자른다. 구분자가 없으면 원본 하나짜리 리스트.
|
||||||
|
pub fn split(s: String, sep: String) -> List[String]
|
||||||
|
|
||||||
|
// split의 반대. concat이 2항이라 여러 조각을 이으면 중첩이 깊어진다.
|
||||||
|
pub fn join(sep: String, parts: List[String]) -> String
|
||||||
|
|
||||||
|
pub fn trim(s: String) -> String
|
||||||
|
pub fn starts_with(s: String, prefix: String) -> Bool
|
||||||
|
pub fn contains(s: String, needle: String) -> Bool
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 표준 라이브러리: 테스트.
|
||||||
|
//
|
||||||
|
// assert는 특별한 것이 아니라 crash 위의 설탕이다. 그래서 본문이 있다 —
|
||||||
|
// std에서 실제 coollang으로 쓰인 첫 함수다.
|
||||||
|
//
|
||||||
|
// 실패 메시지에 값이 안 나오는 것은 의도다. `assert(a == b)`에서 a와 b를
|
||||||
|
// 보여주려면 표현식 텍스트를 잡아야 하고 그건 매크로다. 테스트 이름과
|
||||||
|
// 위치가 어느 것이 깨졌는지 말해준다.
|
||||||
|
|
||||||
|
pub fn assert(c: Bool) {
|
||||||
|
if !c {
|
||||||
|
crash("assert 실패")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,4 +2,10 @@
|
|||||||
(name test_coollang)
|
(name test_coollang)
|
||||||
(libraries coollang)
|
(libraries coollang)
|
||||||
(deps
|
(deps
|
||||||
(glob_files %{workspace_root}/samples/*.cool)))
|
(glob_files %{workspace_root}/samples/*.cool)
|
||||||
|
(glob_files %{workspace_root}/std/*.cool)
|
||||||
|
(glob_files %{workspace_root}/docs/*.ebnf)
|
||||||
|
(glob_files_rec %{workspace_root}/dogfoods/**)
|
||||||
|
(glob_files %{workspace_root}/samples/app/*)
|
||||||
|
(glob_files %{workspace_root}/samples/modules/*.cool)
|
||||||
|
(glob_files %{workspace_root}/samples/run/*.cool)))
|
||||||
|
|||||||
+1442
-12
File diff suppressed because it is too large
Load Diff
@@ -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