std: 마찰 보고의 F1/F3/F6/F7 처리 — 292줄이 244줄로
문법은 건드리지 않았다. 표본이 한 사람이 쓴 300줄 하나뿐인데 되돌리기 비싼 축을 움직일 수는 없다. std 보강만 했다. 추가: List.first/nth, String.join, std/option.cool, std/result.cool. samples/app 재작성 결과 config.cool 196 → 148줄. head_or, second_or, Pick, take_at, first_text, first_entry가 통째로 사라졌다. 예측 40줄, 실제 48줄 — F1의 값이 확인됐다. F3은 줄 수로 값이 안 보인다. main.cool은 96줄 그대로다. 4단 중첩 String.concat이 4줄짜리 String.join 배열이 됐으니 줄 수가 같다. 읽기는 확실히 나아졌다. 줄 수는 읽기 좋음의 대리 지표일 뿐이고 여기서 그 대리가 깨진다 — 다음 개밥 먹기는 다른 것을 재야 한다. F7: Interp.implemented와 std/*.cool 선언이 서로를 덮는지 테스트가 양방향 으로 검사한다. 어긋나면 빌드가 깨진다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
This commit is contained in:
@@ -63,7 +63,7 @@ m101.cool:21:3: match가 모든 경우를 덮지 않습니다 (빠진 경우: Up
|
|||||||
## 실제로 써본 결과
|
## 실제로 써본 결과
|
||||||
|
|
||||||
`samples/app`은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴
|
`samples/app`은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴
|
||||||
프로그램이다 — 설정 파서 + 리포트 도구, 2모듈 292줄.
|
프로그램이다 — 설정 파서 + 리포트 도구, 2모듈 244줄.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ coolc run samples/app/main.cool samples/app/example.conf
|
$ coolc run samples/app/main.cool samples/app/example.conf
|
||||||
|
|||||||
@@ -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이 먼저다.** 둘 다 "언어가 틀렸다"가 아니라
|
v1로 넘길 때 **F1과 F3이 먼저다.** 둘 다 "언어가 틀렸다"가 아니라
|
||||||
|
|||||||
@@ -375,6 +375,9 @@ samples/app — 설정 파서 + 리포트 도구, 2모듈 292줄. 검사기를
|
|||||||
항목 하나는 확인해 보니 관찰자가 틀린 것이었다 — 개밥 먹기의 불편은
|
항목 하나는 확인해 보니 관찰자가 틀린 것이었다 — 개밥 먹기의 불편은
|
||||||
언어의 성질일 수도, 쓴 사람의 습관일 수도 있다.
|
언어의 성질일 수도, 쓴 사람의 습관일 수도 있다.
|
||||||
전문과 증거는 docs/friction.md. 이것이 v1 설계의 첫 입력이다.
|
전문과 증거는 docs/friction.md. 이것이 v1 설계의 첫 입력이다.
|
||||||
|
후속: std 보강(List.first/nth, String.join, Option/Result 함수)으로 292줄이
|
||||||
|
244줄이 됐다. 문법은 건드리지 않았다 — 표본 하나로 되돌리기 비싼 축을
|
||||||
|
움직일 수는 없다.
|
||||||
|
|
||||||
■ 측정 (2026-08, v0 fast path)
|
■ 측정 (2026-08, v0 fast path)
|
||||||
100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현.
|
100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현.
|
||||||
|
|||||||
+86
-7
@@ -144,10 +144,8 @@ let root_capability name : value option =
|
|||||||
match args with
|
match args with
|
||||||
| [ VStr path ] -> (
|
| [ VStr path ] -> (
|
||||||
try VEnum ("Result", "Ok", [ VStr (read_whole path) ])
|
try VEnum ("Result", "Ok", [ VStr (read_whole path) ])
|
||||||
with Sys_error m ->
|
with Sys_error m -> VEnum ("Result", "Err", [ VStr m ]))
|
||||||
VEnum ("Result", "Err", [ VStr m ]))
|
| _ -> VEnum ("Result", "Err", [ VStr "read: 경로가 필요합니다" ]) );
|
||||||
| _ -> VEnum ("Result", "Err", [ VStr "read: 경로가 필요합니다" ])
|
|
||||||
);
|
|
||||||
] ))
|
] ))
|
||||||
| "Args" ->
|
| "Args" ->
|
||||||
Some
|
Some
|
||||||
@@ -160,15 +158,56 @@ let root_capability name : value option =
|
|||||||
Some (VScope "root")
|
Some (VScope "root")
|
||||||
| _ -> None
|
| _ -> 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 =
|
let builtin pos name (args : value list) : value =
|
||||||
match (name, args) with
|
match (name, args) with
|
||||||
| "string.concat", [ VStr a; VStr b ] -> VStr (a ^ b)
|
| "string.concat", [ VStr a; VStr b ] -> VStr (a ^ b)
|
||||||
| "string.len", [ VStr a ] -> VInt (String.length a)
|
| "string.len", [ VStr a ] -> VInt (String.length a)
|
||||||
| "string.is_empty", [ VStr a ] -> VBool (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.trim", [ VStr s ] -> VStr (String.trim s)
|
||||||
| "string.starts_with", [ VStr s; VStr p ] ->
|
| "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)
|
| "string.contains", [ VStr s; VStr n ] -> VBool (find_sub s n <> None)
|
||||||
| "int.show", [ VInt n ] -> VStr (string_of_int n)
|
| "int.show", [ VInt n ] -> VStr (string_of_int n)
|
||||||
| "int.abs", [ VInt n ] -> VInt (abs 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.push", [ VList xs; x ] -> VList (xs @ [ x ])
|
||||||
| "list.concat", [ VList xs; VList ys ] -> VList (xs @ ys)
|
| "list.concat", [ VList xs; VList ys ] -> VList (xs @ ys)
|
||||||
| "list.reverse", [ VList xs ] -> VList (List.rev xs)
|
| "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)
|
fail pos (Printf.sprintf "%s은(는) 런타임이 제공하지 않습니다 (표준 라이브러리가 아직 없습니다)" name)
|
||||||
|
|
||||||
@@ -305,9 +365,28 @@ and apply st pos f args =
|
|||||||
| [ VList xs; f ] ->
|
| [ VList xs; f ] ->
|
||||||
VList
|
VList
|
||||||
(List.filter
|
(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)
|
xs)
|
||||||
| _ -> fail pos "list.filter는 리스트와 함수를 받습니다")
|
| _ -> 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" -> (
|
| VBuiltin "list.fold" -> (
|
||||||
match args with
|
match args with
|
||||||
| [ VList xs; init; f ] ->
|
| [ VList xs; init; f ] ->
|
||||||
|
|||||||
+12
-60
@@ -7,6 +7,8 @@
|
|||||||
import "cool.dev/std/list" as List
|
import "cool.dev/std/list" as List
|
||||||
import "cool.dev/std/string" as String
|
import "cool.dev/std/string" as String
|
||||||
import "cool.dev/std/int" as Int
|
import "cool.dev/std/int" as Int
|
||||||
|
import "cool.dev/std/option" as Option
|
||||||
|
import "cool.dev/std/result" as Result
|
||||||
|
|
||||||
// 설정 값. 타입이 셋뿐이므로 열거형이 맞다.
|
// 설정 값. 타입이 셋뿐이므로 열거형이 맞다.
|
||||||
pub enum Value {
|
pub enum Value {
|
||||||
@@ -75,14 +77,14 @@ pub fn parse_line(cfg: Config, no: Int, raw: String) -> Config {
|
|||||||
cfg
|
cfg
|
||||||
} else if List.len(parts) != 2 {
|
} else if List.len(parts) != 2 {
|
||||||
add_problem(cfg, no, String.concat("= 가 하나여야 합니다: ", line))
|
add_problem(cfg, no, String.concat("= 가 하나여야 합니다: ", line))
|
||||||
} else if String.is_empty(String.trim(head_or(parts, ""))) {
|
} 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, "이름이 비어 있습니다")
|
add_problem(cfg, no, "이름이 비어 있습니다")
|
||||||
} else {
|
} else {
|
||||||
add_entry(cfg, Entry {
|
add_entry(cfg, Entry { line: no, key: key, value: parse_value(val) })
|
||||||
line: no,
|
}
|
||||||
key: String.trim(head_or(parts, "")),
|
|
||||||
value: parse_value(String.trim(second_or(parts, ""))),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 {
|
pub fn parse(text: String) -> Config {
|
||||||
let lines = String.split(text, "\n")
|
let lines = String.split(text, "\n")
|
||||||
let start = Numbered {
|
let start = Numbered {
|
||||||
@@ -154,29 +125,10 @@ pub copyable struct Numbered {
|
|||||||
// 이름으로 찾는다. 없으면 Err — 못 찾은 것은 오류지 빈 값이 아니다.
|
// 이름으로 찾는다. 없으면 Err — 못 찾은 것은 오류지 빈 값이 아니다.
|
||||||
pub fn lookup(cfg: Config, key: String) -> Result[Value, String] {
|
pub fn lookup(cfg: Config, key: String) -> Result[Value, String] {
|
||||||
let hit = List.filter(cfg.entries, fn(e) { e.key == key })
|
let hit = List.filter(cfg.entries, fn(e) { e.key == key })
|
||||||
match first_entry(hit) {
|
Result.map(
|
||||||
Some(e) => Ok(e.value),
|
Option.ok_or(List.first(hit), String.concat("설정에 없습니다: ", key)),
|
||||||
None => Err(String.concat("설정에 없습니다: ", key)),
|
fn(e) { e.value },
|
||||||
}
|
)
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_int(cfg: Config, key: String) -> Result[Int, String] {
|
pub fn get_int(cfg: Config, key: String) -> Result[Int, String] {
|
||||||
|
|||||||
+15
-15
@@ -8,6 +8,7 @@ import "config" as Cfg
|
|||||||
import "cool.dev/std/list" as List
|
import "cool.dev/std/list" as List
|
||||||
import "cool.dev/std/string" as String
|
import "cool.dev/std/string" as String
|
||||||
import "cool.dev/std/int" as Int
|
import "cool.dev/std/int" as Int
|
||||||
|
import "cool.dev/std/option" as Option
|
||||||
|
|
||||||
pub capability Console {
|
pub capability Console {
|
||||||
fn print(s: String) effects {Console.print}
|
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} {
|
pub fn report(c: Console, cfg: Cfg.Config) effects {Console.print} {
|
||||||
c.print("설정")
|
c.print("설정")
|
||||||
List.each(cfg.entries, fn(e) {
|
List.each(cfg.entries, fn(e) {
|
||||||
c.print(String.concat(" ", String.concat(e.key,
|
c.print(String.join("", [
|
||||||
String.concat(" = ", String.concat(Cfg.show_value(e.value),
|
" ", e.key, " = ", Cfg.show_value(e.value),
|
||||||
String.concat(" (", String.concat(Cfg.type_name(e.value), ")")))))))
|
" (", Cfg.type_name(e.value), ")",
|
||||||
|
]))
|
||||||
})
|
})
|
||||||
|
|
||||||
c.print("")
|
c.print("")
|
||||||
c.print(String.concat("항목 ", String.concat(Int.show(List.len(cfg.entries)),
|
c.print(String.join("", [
|
||||||
String.concat("개, 문제 ", String.concat(Int.show(List.len(cfg.problems)), "개")))))
|
"항목 ", Int.show(List.len(cfg.entries)),
|
||||||
|
"개, 문제 ", Int.show(List.len(cfg.problems)), "개",
|
||||||
|
]))
|
||||||
|
|
||||||
if List.is_empty(cfg.problems) {
|
if List.is_empty(cfg.problems) {
|
||||||
c.print("문제 없음")
|
c.print("문제 없음")
|
||||||
@@ -39,8 +43,7 @@ pub fn report(c: Console, cfg: Cfg.Config) effects {Console.print} {
|
|||||||
c.print("")
|
c.print("")
|
||||||
c.print("문제")
|
c.print("문제")
|
||||||
List.each(cfg.problems, fn(p) {
|
List.each(cfg.problems, fn(p) {
|
||||||
c.print(String.concat(" ", String.concat(Int.show(p.line),
|
c.print(String.join("", [ " ", Int.show(p.line), "행: ", p.message ]))
|
||||||
String.concat("행: ", 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} {
|
pub fn show_int(c: Console, cfg: Cfg.Config, key: String) effects {Console.print} {
|
||||||
match Cfg.get_int(cfg, key) {
|
match Cfg.get_int(cfg, key) {
|
||||||
Ok(n) => c.print(String.concat(" ", String.concat(key,
|
Ok(n) => c.print(String.join("", [ " ", key, " = ", Int.show(n) ])),
|
||||||
String.concat(" = ", Int.show(n))))),
|
|
||||||
Err(m) => c.print(String.concat(" ", m)),
|
Err(m) => c.print(String.concat(" ", m)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_flag(c: Console, cfg: Cfg.Config, key: String) effects {Console.print} {
|
pub fn show_flag(c: Console, cfg: Cfg.Config, key: String) effects {Console.print} {
|
||||||
match Cfg.get_flag(cfg, key) {
|
match Cfg.get_flag(cfg, key) {
|
||||||
Ok(b) => c.print(String.concat(" ", String.concat(key,
|
Ok(b) => c.print(String.join("", [
|
||||||
String.concat(" = ", if b { "true" } else { "false" })))),
|
" ", key, " = ", if b { "true" } else { "false" },
|
||||||
|
])),
|
||||||
Err(m) => c.print(String.concat(" ", m)),
|
Err(m) => c.print(String.concat(" ", m)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn first_arg(xs: List[String]) -> Result[String, String] {
|
pub fn first_arg(xs: List[String]) -> Result[String, String] {
|
||||||
match Cfg.first_text(xs) {
|
Option.ok_or(List.first(xs), "설정 파일 경로가 필요합니다")
|
||||||
Some(p) => Ok(p),
|
|
||||||
None => Err("설정 파일 경로가 필요합니다"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main(c: Console, f: File, a: Args)
|
pub fn main(c: Console, f: File, a: Args)
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ pub fn len[a](xs: List[a]) -> Int
|
|||||||
|
|
||||||
pub fn is_empty[a](xs: List[a]) -> Bool
|
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]
|
pub fn push[a](xs: List[a], x: a) -> List[a]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -10,6 +10,9 @@ pub fn concat(a: String, b: String) -> String
|
|||||||
// 구분자로 자른다. 구분자가 없으면 원본 하나짜리 리스트.
|
// 구분자로 자른다. 구분자가 없으면 원본 하나짜리 리스트.
|
||||||
pub fn split(s: String, sep: String) -> List[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 trim(s: String) -> String
|
||||||
pub fn starts_with(s: String, prefix: String) -> Bool
|
pub fn starts_with(s: String, prefix: String) -> Bool
|
||||||
pub fn contains(s: String, needle: String) -> Bool
|
pub fn contains(s: String, needle: String) -> Bool
|
||||||
|
|||||||
+41
-6
@@ -1242,22 +1242,22 @@ let () =
|
|||||||
|
|
||||||
let () =
|
let () =
|
||||||
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
|
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
|
||||||
match Session.run ~args:[ "../samples/app/example.conf" ] st
|
match
|
||||||
"../samples/app/main.cool"
|
Session.run
|
||||||
|
~args:[ "../samples/app/example.conf" ]
|
||||||
|
st "../samples/app/main.cool"
|
||||||
with
|
with
|
||||||
| Error e ->
|
| Error e ->
|
||||||
Printf.printf " (실행 오류: %s)\n" (Session.string_of_error e);
|
Printf.printf " (실행 오류: %s)\n" (Session.string_of_error e);
|
||||||
check "app: 설정 리포트가 돈다" false
|
check "app: 설정 리포트가 돈다" false
|
||||||
| Ok out ->
|
| Ok out ->
|
||||||
check "app: 항목과 문제를 센다"
|
check "app: 항목과 문제를 센다" (has_sub out "항목 5개, 문제 2개");
|
||||||
(has_sub out "항목 5개, 문제 2개");
|
|
||||||
check "app: 값의 타입을 모양으로 정한다"
|
check "app: 값의 타입을 모양으로 정한다"
|
||||||
(has_sub out "threads = 4 (number)"
|
(has_sub out "threads = 4 (number)"
|
||||||
&& has_sub out "verbose = true (flag)"
|
&& has_sub out "verbose = true (flag)"
|
||||||
&& has_sub out "name = \"coollang\" (text)");
|
&& has_sub out "name = \"coollang\" (text)");
|
||||||
check "app: 문제에 줄 번호가 붙는다"
|
check "app: 문제에 줄 번호가 붙는다"
|
||||||
(has_sub out "10행: 이름이 비어 있습니다"
|
(has_sub out "10행: 이름이 비어 있습니다" && has_sub out "11행: = 가 하나여야 합니다");
|
||||||
&& has_sub out "11행: = 가 하나여야 합니다");
|
|
||||||
check "app: 타입 있는 조회" (has_sub out "port = 8080")
|
check "app: 타입 있는 조회" (has_sub out "port = 8080")
|
||||||
|
|
||||||
let () =
|
let () =
|
||||||
@@ -1271,3 +1271,38 @@ let () =
|
|||||||
match Session.run ~args:[ "/없는/파일.conf" ] st "../samples/app/main.cool" with
|
match Session.run ~args:[ "/없는/파일.conf" ] st "../samples/app/main.cool" with
|
||||||
| Ok out -> check "app: 없는 파일을 Err로 돌려준다" (has_sub out "오류: ")
|
| Ok out -> check "app: 없는 파일을 Err로 돌려준다" (has_sub out "오류: ")
|
||||||
| Error _ -> check "app: 없는 파일을 Err로 돌려준다" false
|
| 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
|
||||||
|
|||||||
Reference in New Issue
Block a user