From a9f65278252117378077ff811f083dd06a599d26 Mon Sep 17 00:00:00 2001 From: coolguy Date: Sun, 30 Aug 2026 16:20:23 +0900 Subject: [PATCH] =?UTF-8?q?std:=20=EB=A7=88=EC=B0=B0=20=EB=B3=B4=EA=B3=A0?= =?UTF-8?q?=EC=9D=98=20F1/F3/F6/F7=20=EC=B2=98=EB=A6=AC=20=E2=80=94=20292?= =?UTF-8?q?=EC=A4=84=EC=9D=B4=20244=EC=A4=84=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문법은 건드리지 않았다. 표본이 한 사람이 쓴 300줄 하나뿐인데 되돌리기 비싼 축을 움직일 수는 없다. std 보강만 했다. 추가: List.first/nth, String.join, std/option.cool, std/result.cool. samples/app 재작성 결과 config.cool 196 → 148줄. head_or, second_or, Pick, take_at, first_text, first_entry가 통째로 사라졌다. 예측 40줄, 실제 48줄 — F1의 값이 확인됐다. F3은 줄 수로 값이 안 보인다. main.cool은 96줄 그대로다. 4단 중첩 String.concat이 4줄짜리 String.join 배열이 됐으니 줄 수가 같다. 읽기는 확실히 나아졌다. 줄 수는 읽기 좋음의 대리 지표일 뿐이고 여기서 그 대리가 깨진다 — 다음 개밥 먹기는 다른 것을 재야 한다. F7: Interp.implemented와 std/*.cool 선언이 서로를 덮는지 테스트가 양방향 으로 검사한다. 어긋나면 빌드가 깨진다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E --- README.md | 2 +- docs/friction.md | 61 +++++++++++++++++++++++++++ docs/thesis.md | 3 ++ lib/interp.ml | 93 +++++++++++++++++++++++++++++++++++++---- samples/app/config.cool | 74 ++++++-------------------------- samples/app/main.cool | 30 ++++++------- std/list.cool | 7 ++++ std/option.cool | 15 +++++++ std/result.cool | 15 +++++++ std/string.cool | 3 ++ test/test_coollang.ml | 47 ++++++++++++++++++--- 11 files changed, 260 insertions(+), 90 deletions(-) create mode 100644 std/option.cool create mode 100644 std/result.cool diff --git a/README.md b/README.md index 03072bf..481dd54 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ m101.cool:21:3: match가 모든 경우를 덮지 않습니다 (빠진 경우: Up ## 실제로 써본 결과 `samples/app`은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴 -프로그램이다 — 설정 파서 + 리포트 도구, 2모듈 292줄. +프로그램이다 — 설정 파서 + 리포트 도구, 2모듈 244줄. ``` $ coolc run samples/app/main.cool samples/app/example.conf diff --git a/docs/friction.md b/docs/friction.md index 96a9ed6..3f5239b 100644 --- a/docs/friction.md +++ b/docs/friction.md @@ -184,6 +184,67 @@ pub copyable struct Numbered { --- +--- + +## 후속 (같은 날) + +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`의 선언이 +서로를 덮는지 테스트가 양방향으로 검사한다. 어긋나면 빌드가 깨진다. + +남은 것: F4(struct 부분 갱신), F5(fold 인덱스). 둘 다 문법 결정이거나 +튜플이 필요하다. 표본을 더 모은 뒤에 판단한다. + +--- + ## 결론 v1로 넘길 때 **F1과 F3이 먼저다.** 둘 다 "언어가 틀렸다"가 아니라 diff --git a/docs/thesis.md b/docs/thesis.md index 391c300..1cbb1b1 100644 --- a/docs/thesis.md +++ b/docs/thesis.md @@ -375,6 +375,9 @@ samples/app — 설정 파서 + 리포트 도구, 2모듈 292줄. 검사기를 항목 하나는 확인해 보니 관찰자가 틀린 것이었다 — 개밥 먹기의 불편은 언어의 성질일 수도, 쓴 사람의 습관일 수도 있다. 전문과 증거는 docs/friction.md. 이것이 v1 설계의 첫 입력이다. +후속: std 보강(List.first/nth, String.join, Option/Result 함수)으로 292줄이 +244줄이 됐다. 문법은 건드리지 않았다 — 표본 하나로 되돌리기 비싼 축을 +움직일 수는 없다. ■ 측정 (2026-08, v0 fast path) 100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현. diff --git a/lib/interp.ml b/lib/interp.ml index f37a1d9..7e254a7 100644 --- a/lib/interp.ml +++ b/lib/interp.ml @@ -144,10 +144,8 @@ let root_capability name : value option = 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: 경로가 필요합니다" ]) - ); + with Sys_error m -> VEnum ("Result", "Err", [ VStr m ])) + | _ -> VEnum ("Result", "Err", [ VStr "read: 경로가 필요합니다" ]) ); ] )) | "Args" -> Some @@ -160,15 +158,56 @@ let root_capability name : value option = Some (VScope "root") | _ -> None +(* 런타임이 구현한 이름 전부. std/*.cool의 선언과 이 목록이 어긋나면 검사는 + 통과하고 실행이 죽는다 — 그 간극을 테스트가 막는다 (docs/friction.md F7). + v1에서 std를 coollang으로 구현하면 이 목록 자체가 사라진다. *) +let implemented = + [ + "string.len"; + "string.is_empty"; + "string.concat"; + "string.split"; + "string.join"; + "string.trim"; + "string.starts_with"; + "string.contains"; + "int.show"; + "int.abs"; + "int.parse"; + "bool.show"; + "list.len"; + "list.is_empty"; + "list.first"; + "list.nth"; + "list.push"; + "list.concat"; + "list.reverse"; + "list.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.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) + 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) @@ -182,6 +221,27 @@ let builtin pos name (args : value list) : value = | "list.push", [ VList xs; x ] -> VList (xs @ [ x ]) | "list.concat", [ VList xs; VList ys ] -> VList (xs @ ys) | "list.reverse", [ VList xs ] -> VList (List.rev xs) + | "list.first", [ VList xs ] -> ( + match xs with + | [] -> VEnum ("Option", "None", []) + | x :: _ -> VEnum ("Option", "Some", [ x ])) + | "list.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) @@ -305,9 +365,28 @@ and apply st pos f args = | [ VList xs; f ] -> VList (List.filter - (fun x -> match apply st pos f [ x ] with VBool b -> b | _ -> false) + (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 ] -> diff --git a/samples/app/config.cool b/samples/app/config.cool index c0de347..3c42609 100644 --- a/samples/app/config.cool +++ b/samples/app/config.cool @@ -7,6 +7,8 @@ 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 // 설정 값. 타입이 셋뿐이므로 열거형이 맞다. pub enum Value { @@ -75,14 +77,14 @@ pub fn parse_line(cfg: Config, no: Int, raw: String) -> Config { cfg } else if List.len(parts) != 2 { add_problem(cfg, no, String.concat("= 가 하나여야 합니다: ", line)) - } else if String.is_empty(String.trim(head_or(parts, ""))) { - add_problem(cfg, no, "이름이 비어 있습니다") } else { - add_entry(cfg, Entry { - line: no, - key: String.trim(head_or(parts, "")), - value: parse_value(String.trim(second_or(parts, ""))), - }) + 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) }) + } } } @@ -100,37 +102,6 @@ pub fn add_problem(cfg: Config, no: Int, msg: String) -> Config { } } -// 인덱싱 연산자가 없으므로 앞의 둘을 꺼내는 일은 이름 있는 함수가 한다. -pub fn head_or(xs: List[String], fallback: String) -> String { - List.fold(xs, Pick { taken: false, at: 0, found: fallback }, fn(p, x) { - take_at(p, x, 0) - }).found -} - -pub fn second_or(xs: List[String], fallback: String) -> String { - List.fold(xs, Pick { taken: false, at: 0, found: fallback }, fn(p, x) { - take_at(p, x, 1) - }).found -} - -// fold로 n번째를 고른다. 셋 다 필요하다 — 몇 번째를 보고 있는지(at), -// 이미 골랐는지(taken), 무엇을 골랐는지(found). -pub copyable struct Pick { - taken: Bool, - at: Int, - found: String, -} - -pub fn take_at(p: Pick, x: String, want: Int) -> Pick { - if p.taken { - Pick { taken: true, at: p.at + 1, found: p.found } - } else if p.at == want { - Pick { taken: true, at: p.at + 1, found: x } - } else { - Pick { taken: false, at: p.at + 1, found: p.found } - } -} - pub fn parse(text: String) -> Config { let lines = String.split(text, "\n") let start = Numbered { @@ -154,29 +125,10 @@ pub copyable struct Numbered { // 이름으로 찾는다. 없으면 Err — 못 찾은 것은 오류지 빈 값이 아니다. pub fn lookup(cfg: Config, key: String) -> Result[Value, String] { let hit = List.filter(cfg.entries, fn(e) { e.key == key }) - match first_entry(hit) { - Some(e) => Ok(e.value), - None => Err(String.concat("설정에 없습니다: ", key)), - } -} - -pub fn first_entry(xs: List[Entry]) -> Option[Entry] { - List.fold(xs, None, fn(acc, e) { - match acc { - Some(prev) => Some(prev), - None => Some(e), - } - }) -} - -// 리스트의 첫 원소. fold로 쓴다 — 언어에 인덱싱이 없다. -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), - } - }) + 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] { diff --git a/samples/app/main.cool b/samples/app/main.cool index aa8df4d..8a4d1bc 100644 --- a/samples/app/main.cool +++ b/samples/app/main.cool @@ -8,6 +8,7 @@ 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} @@ -24,14 +25,17 @@ pub capability Args { pub fn report(c: Console, cfg: Cfg.Config) effects {Console.print} { c.print("설정") List.each(cfg.entries, fn(e) { - 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), ")", + ])) }) c.print("") - c.print(String.concat("항목 ", String.concat(Int.show(List.len(cfg.entries)), - String.concat("개, 문제 ", String.concat(Int.show(List.len(cfg.problems)), "개"))))) + c.print(String.join("", [ + "항목 ", Int.show(List.len(cfg.entries)), + "개, 문제 ", Int.show(List.len(cfg.problems)), "개", + ])) if List.is_empty(cfg.problems) { c.print("문제 없음") @@ -39,8 +43,7 @@ pub fn report(c: Console, cfg: Cfg.Config) effects {Console.print} { c.print("") c.print("문제") List.each(cfg.problems, fn(p) { - c.print(String.concat(" ", String.concat(Int.show(p.line), - String.concat("행: ", p.message)))) + c.print(String.join("", [ " ", Int.show(p.line), "행: ", p.message ])) }) } } @@ -56,25 +59,22 @@ pub fn check_known(c: Console, cfg: Cfg.Config) effects {Console.print} { 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.concat(" ", String.concat(key, - String.concat(" = ", Int.show(n))))), + 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.concat(" ", String.concat(key, - String.concat(" = ", if b { "true" } else { "false" })))), + 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] { - match Cfg.first_text(xs) { - Some(p) => Ok(p), - None => Err("설정 파일 경로가 필요합니다"), - } + Option.ok_or(List.first(xs), "설정 파일 경로가 필요합니다") } pub fn main(c: Console, f: File, a: Args) diff --git a/std/list.cool b/std/list.cool index 00c8bc1..ba2b5a1 100644 --- a/std/list.cool +++ b/std/list.cool @@ -11,6 +11,13 @@ 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] + // 뒤에 하나 붙인 새 리스트. 제자리 수정이 아니다. pub fn push[a](xs: List[a], x: a) -> List[a] diff --git a/std/option.cool b/std/option.cool new file mode 100644 index 0000000..ecff1bc --- /dev/null +++ b/std/option.cool @@ -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] diff --git a/std/result.cool b/std/result.cool new file mode 100644 index 0000000..358ed08 --- /dev/null +++ b/std/result.cool @@ -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 diff --git a/std/string.cool b/std/string.cool index 6fb6c1e..238305a 100644 --- a/std/string.cool +++ b/std/string.cool @@ -10,6 +10,9 @@ 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 diff --git a/test/test_coollang.ml b/test/test_coollang.ml index 88d6622..f9c357a 100644 --- a/test/test_coollang.ml +++ b/test/test_coollang.ml @@ -1242,22 +1242,22 @@ let () = let () = let st = Session.create ~root:"../samples/app" ~std:"../std" () in - match Session.run ~args:[ "../samples/app/example.conf" ] st - "../samples/app/main.cool" + match + Session.run + ~args:[ "../samples/app/example.conf" ] + st "../samples/app/main.cool" with | Error e -> Printf.printf " (실행 오류: %s)\n" (Session.string_of_error e); check "app: 설정 리포트가 돈다" false | Ok out -> - check "app: 항목과 문제를 센다" - (has_sub out "항목 5개, 문제 2개"); + check "app: 항목과 문제를 센다" (has_sub out "항목 5개, 문제 2개"); check "app: 값의 타입을 모양으로 정한다" (has_sub out "threads = 4 (number)" && has_sub out "verbose = true (flag)" && has_sub out "name = \"coollang\" (text)"); check "app: 문제에 줄 번호가 붙는다" - (has_sub out "10행: 이름이 비어 있습니다" - && has_sub out "11행: = 가 하나여야 합니다"); + (has_sub out "10행: 이름이 비어 있습니다" && has_sub out "11행: = 가 하나여야 합니다"); check "app: 타입 있는 조회" (has_sub out "port = 8080") let () = @@ -1271,3 +1271,38 @@ let () = match Session.run ~args:[ "/없는/파일.conf" ] st "../samples/app/main.cool" with | Ok out -> check "app: 없는 파일을 Err로 돌려준다" (has_sub out "오류: ") | Error _ -> check "app: 없는 파일을 Err로 돌려준다" false + +(* std 선언과 런타임 구현이 어긋나면 검사는 통과하고 실행이 죽는다. + v0에서 둘은 다른 파일에 있으므로 일치는 테스트가 지킨다 (friction F7). *) +let () = + let dir = "../std" in + let declared = + Sys.readdir dir |> Array.to_list + |> List.filter (fun f -> Filename.check_suffix f ".cool") + |> List.concat_map (fun f -> + let m = ref "" in + (match Driver.ast (Filename.concat dir f) with + | Error _ -> () + | Ok _ -> m := Filename.remove_extension f); + match Driver.ast (Filename.concat dir f) with + | Error _ -> [] + | Ok ast -> + List.filter_map + (function + | Ast.I_fn { decl; _ } -> Some (!m ^ "." ^ decl.fn_name) + | _ -> None) + ast.Ast.items) + in + check "std에 선언이 있다" (List.length declared > 20); + List.iter + (fun name -> + check + (Printf.sprintf "std 선언 %s에 런타임 구현이 있다" name) + (List.mem name Interp.implemented)) + declared; + List.iter + (fun name -> + check + (Printf.sprintf "런타임 구현 %s에 std 선언이 있다" name) + (List.mem name declared)) + Interp.implemented